Skip to content

Commit

Permalink
Add energy
Browse files Browse the repository at this point in the history
  • Loading branch information
Bre77 committed Jul 2, 2024
1 parent 62f094e commit 3a95607
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 47 deletions.
40 changes: 40 additions & 0 deletions src/energy-services/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Logging, PlatformAccessory, Service, WithUUID } from "homebridge";
import { EnergySpecific, } from "tesla-fleet-api";
import { TeslaFleetApiPlatform } from "../platform.js";
import { EventEmitter } from "../utils/event.js";
import { EnergyAccessory, EnergyContext, EnergyDataEvent } from "../energy.js";

export abstract class BaseService {
protected service: Service;
protected log: Logging;
protected platform: TeslaFleetApiPlatform;
protected accessory: PlatformAccessory<EnergyContext>;
protected emitter: EventEmitter<EnergyDataEvent>;
protected energy: EnergySpecific;

constructor(
protected parent: EnergyAccessory,
definition: WithUUID<typeof Service>,
name: string,
subtype: string,
) {
this.log = parent.platform.log;
this.platform = parent.platform;
this.accessory = parent.accessory;
this.emitter = parent.emitter;
this.energy = parent.energy;

name = parent.platform.config.prefixName ? `${this.parent.accessory.displayName} ${name}` : name;

this.service =
this.accessory.getServiceById(definition, subtype) ||
this.accessory.addService(definition, name, subtype);

// Set the configured name if it's not already set since Homekit wont use the display name
const ConfiguredName = this.service.getCharacteristic(this.platform.Characteristic.ConfiguredName);
if (!ConfiguredName.value) {
this.log.debug(`Configured name changing to ${name}`);
ConfiguredName.updateValue(name);
}
}
}
46 changes: 46 additions & 0 deletions src/energy-services/battery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { EnergyAccessory } from "../energy.js";
import { BaseService } from "./base.js";

export class BatteryService extends BaseService {
constructor(parent: EnergyAccessory) {
super(parent, parent.platform.Service.Battery, "Battery", "battery");

const batteryLevel = this.service
.getCharacteristic(this.parent.platform.Characteristic.BatteryLevel);

const chargingState = this.service
.getCharacteristic(this.parent.platform.Characteristic.ChargingState);

const lowBattery = this.service
.getCharacteristic(this.parent.platform.Characteristic.StatusLowBattery);

this.parent.emitter.on("live_status", (data) => {
batteryLevel.updateValue(this.getLevel(data));
chargingState.updateValue(this.getChargingState(data));
lowBattery.updateValue(this.getLowBattery(data));
});
}

getLevel(data): number {
return data.charge_state?.battery_level ?? 50;
}

getChargingState(data): number {
switch (data.charge_state?.charging_state) {
case "Starting":
return this.parent.platform.Characteristic.ChargingState.CHARGING;
case "Charging":
return this.parent.platform.Characteristic.ChargingState.CHARGING;
case "Disconnected":
return this.parent.platform.Characteristic.ChargingState.NOT_CHARGEABLE;
case "NoPower":
return this.parent.platform.Characteristic.ChargingState.NOT_CHARGEABLE;
default:
return this.parent.platform.Characteristic.ChargingState.NOT_CHARGING;
}
}

getLowBattery(data): boolean {
return this.getLevel(data) <= 20;
}
}
22 changes: 22 additions & 0 deletions src/energy-services/changefromgrid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { EnergyAccessory } from "../energy.js";
import { BaseService } from "./base.js";

export class ChargeFromGrid extends BaseService {
constructor(parent: EnergyAccessory) {
super(parent, parent.platform.Service.Switch, "Charge From Grid", "chargefromgrid");

const on = this.service
.getCharacteristic(this.parent.platform.Characteristic.On)
.onSet(async (value) => {
if (typeof value === "boolean") {
await this.energy.grid_import_export().then(() => on.updateValue(value));
}
});

this.parent.emitter.on("site_info", (data) => {
if (typeof data.components.disallow_charge_from_grid_with_solar_installed === "boolean") {
on.updateValue(data.components.disallow_charge_from_grid_with_solar_installed);
}
});
}
}
82 changes: 82 additions & 0 deletions src/energy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { PlatformAccessory } from "homebridge";

import { EnergySpecific } from "tesla-fleet-api";
import {
LiveStatusResponse
} from "tesla-fleet-api/dist/types/live_status";
import {
SiteInfoResponse
} from "tesla-fleet-api/dist/types/site_info.js";
import { TeslaFleetApiPlatform } from "./platform.js";
import { REFRESH_INTERVAL } from "./settings.js";
import { EventEmitter } from "./utils/event.js";
import { ChargeFromGrid } from "./energy-services/changefromgrid.js";

export type EnergyContext = {
id: number;
battery: boolean;
grid: boolean;
solar: boolean;
};

export interface EnergyDataEvent {
live_status(data: LiveStatusResponse): void;
site_info(data: SiteInfoResponse): void;
}

export class EnergyAccessory {
public energy: EnergySpecific;
public emitter: EventEmitter<EnergyDataEvent>;

constructor(
public readonly platform: TeslaFleetApiPlatform,
public readonly accessory: PlatformAccessory<EnergyContext>
) {
if (!this.platform.TeslaFleetApi?.energy) {
throw new Error("TeslaFleetApi not initialized");
}

this.energy = this.platform.TeslaFleetApi.energy.specific(
this.accessory.context.id
);

this.emitter = new EventEmitter();

// Create services
if (this.accessory.context.battery) {
new ChargeFromGrid(this);
}

// Get data and schedule refresh

this.refresh();
setInterval(() => this.refresh(), REFRESH_INTERVAL);
}

async refresh(): Promise<void> {
this.energy
.site_info()
.then((data) => {
this.emitter.emit("site_info", data);
})
.catch(({ status, data }) => {
if (data?.error) {
this.platform.log.warn(`${this.accessory.displayName} site_info return status ${status}: ${data.error}`);
return;
}
this.platform.log.error(`${this.accessory.displayName} site_info return status ${status}: ${data}`);
});
this.energy
.live_status()
.then((data) => {
this.emitter.emit("live_status", data);
})
.catch(({ status, data }) => {
if (data?.error) {
this.platform.log.warn(`${this.accessory.displayName} live_status return status ${status}: ${data.error}`);
return;
}
this.platform.log.error(`${this.accessory.displayName} live_status return status ${status}: ${data}`);
});
}
}
128 changes: 84 additions & 44 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {

import { PLATFORM_NAME, PLUGIN_NAME } from "./settings.js";
import { VehicleAccessory, VehicleContext } from "./vehicle.js";
import { EnergyAccessory, EnergyContext } from "./energy.js";

import { Teslemetry } from "tesla-fleet-api";

Expand All @@ -25,7 +26,7 @@ export class TeslaFleetApiPlatform implements DynamicPlatformPlugin {
public readonly TeslaFleetApi: Teslemetry;

// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory<VehicleContext>[] = [];
public readonly accessories: PlatformAccessory<VehicleContext | EnergyContext>[] = [];

constructor(
public readonly log: Logging,
Expand All @@ -52,57 +53,96 @@ export class TeslaFleetApiPlatform implements DynamicPlatformPlugin {
this.api.on("didFinishLaunching", async () => {
log.debug("Executed didFinishLaunching callback");
// run the method to discover / register your devices as accessories
const newAccessories: PlatformAccessory<VehicleContext>[] = [];
this.TeslaFleetApi.products_by_type()

const { scopes } = await this.TeslaFleetApi.metadata();

await this.TeslaFleetApi.products_by_type()
.then(async ({ vehicles, energy_sites }) => {
vehicles.forEach(async (product) => {
this.TeslaFleetApi.vehicle!;
const uuid = this.api.hap.uuid.generate(`${PLATFORM_NAME}:${product.vin}`);
const cachedAccessory = this.accessories.find(
(accessory) => accessory.UUID === uuid
);
if (cachedAccessory) {
cachedAccessory.context.state = product.state;
cachedAccessory.displayName = product.display_name;
this.log.debug(
"Restoring existing accessory from cache:",
cachedAccessory.displayName
);
new VehicleAccessory(this, cachedAccessory);
return;
}

this.log.debug("Adding new accessory:", product.display_name);
const newAccessory = new this.api.platformAccessory<VehicleContext>(
product.display_name,
uuid,
Categories.OTHER
);
newAccessory.context.vin = product.vin;
newAccessory.context.state = product.state;
newAccessory.displayName = product.display_name;


new VehicleAccessory(this, newAccessory);
newAccessories.push(newAccessory);
});

energy_sites.forEach((product) => { });


}).then(() => this.api.registerPlatformAccessories(
PLUGIN_NAME,
PLATFORM_NAME,
newAccessories
));
const newAccessories: PlatformAccessory<VehicleContext | EnergyContext>[] = [];
if (scopes.includes("vehicle_device_data")) {
//const newVehicleAccessories: PlatformAccessory<VehicleContext>[] = [];
vehicles.forEach(async (product) => {
this.TeslaFleetApi.vehicle!;
const uuid = this.api.hap.uuid.generate(`${PLATFORM_NAME}:${product.vin}`);
let accessory = this.accessories.find(
(accessory) => accessory.UUID === uuid
) as PlatformAccessory<VehicleContext> | undefined;

if (accessory) {
this.log.debug(
"Restoring existing accessory from cache:",
accessory.displayName
);
} else {
this.log.debug("Adding new accessory:", product.display_name);
accessory = new this.api.platformAccessory<VehicleContext>(
product.display_name,
uuid,
Categories.OTHER
);
newAccessories.push(accessory);
}

accessory.context.vin = product.vin;
accessory.context.state = product.state;
accessory.displayName = product.display_name;

new VehicleAccessory(this, accessory);
});
}

if (scopes.includes("energy_device_data")) {
//const newEnergyAccessories: PlatformAccessory<EnergyContext>[] = [];
energy_sites.forEach((product) => {
this.TeslaFleetApi.energy!;
const uuid = this.api.hap.uuid.generate(`${PLATFORM_NAME}:${product.id}`);
let accessory = this.accessories.find(
(accessory) => accessory.UUID === uuid

) as PlatformAccessory<EnergyContext> | undefined;

if (accessory) {
this.log.debug(
"Restoring existing accessory from cache:",
accessory.displayName
);
} else {
this.log.debug("Adding new accessory:", product.site_name);
accessory = new this.api.platformAccessory<EnergyContext>(
product.site_name,
uuid,
Categories.OTHER
)
newAccessories.push(accessory);
}

accessory.context.id = product.energy_site_id;
accessory.context.battery = product.components.battery;
accessory.context.grid = product.components.grid;
accessory.context.solar = product.components.solar;
accessory.displayName = product.site_name;

new EnergyAccessory(this, accessory);
})
}

return newAccessories

}).then((newAccessories) => {
this.api.registerPlatformAccessories(
PLUGIN_NAME,
PLATFORM_NAME,
newAccessories
)
})
});
}

/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to set up event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory<VehicleContext>) {
configureAccessory(accessory: PlatformAccessory<VehicleContext | EnergyContext>) {
this.log.debug("Loading accessory from cache:", accessory.displayName);

// add the restored accessory to the accessories cache, so we can track if it has already been registered
Expand Down
3 changes: 0 additions & 3 deletions src/vehicle-services/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ export abstract class BaseService {
if (!ConfiguredName.value) {
this.log.debug(`Configured name changing to ${name}`);
ConfiguredName.updateValue(name);
} else {
this.log.debug(`Configured name of ${name} is already set to ${ConfiguredName.value}`);
}

}
}

0 comments on commit 3a95607

Please sign in to comment.