diff --git a/src/platform.ts b/src/platform.ts index cacff84..91cbf5b 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -9,7 +9,7 @@ import { } from "homebridge"; import { PLATFORM_NAME, PLUGIN_NAME } from "./settings.js"; -import { VehicleAccessory } from "./vehicle.js"; +import { VehicleAccessory, VehicleContext } from "./vehicle.js"; import { Teslemetry } from "tesla-fleet-api"; @@ -24,7 +24,7 @@ export class TeslaFleetApiPlatform implements DynamicPlatformPlugin { public readonly TeslaFleetApi: Teslemetry; // this is used to track restored cached accessories - public readonly accessories: PlatformAccessory[] = []; + public readonly accessories: PlatformAccessory[] = []; constructor( public readonly log: Logging, @@ -59,7 +59,7 @@ export class TeslaFleetApiPlatform implements DynamicPlatformPlugin { * 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) { + configureAccessory(accessory: PlatformAccessory) { this.log.info("Loading accessory from cache:", accessory.displayName); // add the restored accessory to the accessories cache, so we can track if it has already been registered @@ -90,11 +90,10 @@ export class TeslaFleetApiPlatform implements DynamicPlatformPlugin { } this.log.info("Adding new accessory:", product.display_name); - const newAccessory = new this.api.platformAccessory( + const newAccessory = new this.api.platformAccessory( product.display_name, uuid ); - newAccessory.context.vin = product.vin; newAccessory.context.state = product.state; newAccessory.displayName = product.display_name; diff --git a/src/services/battery.ts b/src/services/battery.ts index d29b742..8026b95 100644 --- a/src/services/battery.ts +++ b/src/services/battery.ts @@ -1,5 +1,4 @@ -import { Characteristic, Service } from "homebridge"; -import { VehicleDataResponse } from "tesla-fleet-api/dist/types/vehicle_data.js"; +import { Service } from "homebridge"; import { VehicleAccessory } from "../vehicle.js"; export class BatteryService { @@ -12,29 +11,29 @@ export class BatteryService { const batteryLevel = this.service .getCharacteristic(this.parent.platform.Characteristic.BatteryLevel) - .onGet(() => this.getLevel(this.parent.accessory.context.data)); + .onGet(this.getLevel); const chargingState = this.service .getCharacteristic(this.parent.platform.Characteristic.ChargingState) - .onGet(() => this.getChargingState(this.parent.accessory.context.data)); + .onGet(this.getChargingState); const lowBattery = this.service .getCharacteristic(this.parent.platform.Characteristic.StatusLowBattery) - .onGet(() => this.getLowBattery(this.parent.accessory.context.data)); + .onGet(this.getLowBattery); - this.parent.emitter.on("vehicle_data", (data) => { - batteryLevel.updateValue(this.getLevel(data)); - chargingState.updateValue(this.getChargingState(data)); - lowBattery.updateValue(this.getLowBattery(data)); + this.parent.emitter.on("vehicle_data", () => { + batteryLevel.updateValue(this.getLevel()); + chargingState.updateValue(this.getChargingState()); + lowBattery.updateValue(this.getLowBattery()); }); } - getLevel(data: VehicleDataResponse): number { - return data?.charge_state?.battery_level ?? 50; + getLevel(): number { + return this.parent.accessory.context?.charge_state?.battery_level ?? 50; } - getChargingState(data: VehicleDataResponse): number { - switch (data?.charge_state?.charging_state) { + getChargingState(): number { + switch (this.parent.accessory.context?.charge_state?.charging_state) { case "Starting": return this.parent.platform.Characteristic.ChargingState.CHARGING; case "Charging": @@ -48,9 +47,7 @@ export class BatteryService { } } - getLowBattery(data: VehicleDataResponse): boolean { - return data?.charge_state?.battery_level - ? data.charge_state.battery_level <= 20 - : false; + getLowBattery(): boolean { + return this.getLevel() <= 20; } } diff --git a/src/services/door.ts b/src/services/door.ts new file mode 100644 index 0000000..69a3300 --- /dev/null +++ b/src/services/door.ts @@ -0,0 +1,52 @@ +import { CharacteristicValue, Service } from "homebridge"; +import { VehicleAccessory } from "../vehicle.js"; + +export class DoorService { + service: Service; + key: "ft" | "rt"; + + constructor( + private parent: VehicleAccessory, + private trunk: "front" | "rear" + ) { + this.service = + this.parent.accessory.getService(this.parent.platform.Service.Door) || + this.parent.accessory.addService(this.parent.platform.Service.Door); + + this.key = this.trunk === "front" ? "ft" : "rt"; + + const currentPosition = this.service + .getCharacteristic(this.parent.platform.Characteristic.CurrentPosition) + .onGet(this.getPosition); + + /*const positionState = this.service + .getCharacteristic(this.parent.platform.Characteristic.PositionState) + .onGet(() => this.getChargingState());*/ + + const targetPosition = this.service + .getCharacteristic(this.parent.platform.Characteristic.TargetPosition) + .onGet(this.getPosition) + .onSet(this.setPosition); + + this.parent.emitter.on("vehicle_data", () => { + currentPosition.updateValue(this.getPosition()); + targetPosition.updateValue(this.getPosition()); + }); + } + + getPosition(): number { + return this.parent.accessory.context?.vehicle_state?.[this.key] ? 100 : 0; + } + + async setPosition(value: CharacteristicValue) { + console.log(value); + const position = this.getPosition(); + if ( + (position === 0 && value === 100) || + (position === 100 && value === 0 && this.trunk === "rear") + ) { + return this.parent.vehicle.actuate_truck(this.trunk).then(() => value); + } + return position; + } +} diff --git a/src/services/information.ts b/src/services/information.ts new file mode 100644 index 0000000..8895276 --- /dev/null +++ b/src/services/information.ts @@ -0,0 +1,59 @@ +// https://developers.homebridge.io/#/service/AccessoryInformation + +import { CharacteristicValue, Service } from "homebridge"; +import { VehicleAccessory } from "../vehicle.js"; + +export class AccessoryInformationService { + service: Service; + + constructor(private parent: VehicleAccessory) { + this.service = + this.parent.accessory.getService( + this.parent.platform.Service.AccessoryInformation + ) || + this.parent.accessory.addService( + this.parent.platform.Service.AccessoryInformation + ); + + this.service // Move this to a separate service + .setCharacteristic( + this.parent.platform.Characteristic.Manufacturer, + "Tesla" + ) + .setCharacteristic( + this.parent.platform.Characteristic.Model, + this.parent.vehicle.model + ) + .setCharacteristic( + this.parent.platform.Characteristic.SerialNumber, + this.parent.vehicle.vin + ); + + const version = this.service + .getCharacteristic(this.parent.platform.Characteristic.FirmwareRevision) + .onGet(this.getVersion); + + this.service + .getCharacteristic(this.parent.platform.Characteristic.Identify) + .onSet(this.setIdentify); + + this.parent.emitter.on("vehicle_data", () => { + version.updateValue(this.getVersion()); + }); + } + + getVersion(): string { + return ( + this.parent.accessory.context?.vehicle_state?.software_update?.version ?? + "unknown" + ); + } + + async setIdentify(value: CharacteristicValue) { + console.log(value); + return this.parent + .wake_up() + .then(() => this.parent.vehicle.flash_lights()) + .then(() => false); + } +} diff --git a/src/event.ts b/src/utils/event.ts similarity index 100% rename from src/event.ts rename to src/utils/event.ts diff --git a/src/utils/wake.ts b/src/utils/wake.ts new file mode 100644 index 0000000..d7e7ef2 --- /dev/null +++ b/src/utils/wake.ts @@ -0,0 +1,9 @@ +/* + * Returns a Promise that waits for the given number of milliseconds + * (via setTimeout), then resolves. + */ +export async function wait(ms: number = 0) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/src/vehicle.ts b/src/vehicle.ts index 06d571c..e328d7c 100644 --- a/src/vehicle.ts +++ b/src/vehicle.ts @@ -1,24 +1,43 @@ import { CharacteristicValue, PlatformAccessory, Service } from "homebridge"; import { VehicleSpecific } from "tesla-fleet-api"; +import { + ChargeState, + ClimateState, + DriveState, + GUISettings, + VehicleConfig, + VehicleState, +} from "tesla-fleet-api/dist/types/vehicle_data"; import { VehicleDataResponse } from "tesla-fleet-api/dist/types/vehicle_data.js"; -import { EventEmitter } from "./event.js"; import { TeslaFleetApiPlatform } from "./platform.js"; import { BatteryService } from "./services/battery.js"; +import { AccessoryInformationService } from "./services/information.js"; import { REFRESH_INTERVAL } from "./settings.js"; +import { EventEmitter } from "./utils/event.js"; -export interface VehicleData { - vehicle_data(data: VehicleDataResponse): void; +export type VehicleContext = { + vin: string; + state: string; + charge_state: ChargeState; + climate_state: ClimateState; + drive_state: DriveState; + gui_settings: GUISettings; + vehicle_config: VehicleConfig; + vehicle_state: VehicleState; +}; + +export interface VehicleDataEvent { + vehicle_data(data: VehicleContext): void; } export class VehicleAccessory { - private vehicle: VehicleSpecific; - public emitter: EventEmitter; - private information: Service; + public vehicle: VehicleSpecific; + public emitter: EventEmitter; constructor( public readonly platform: TeslaFleetApiPlatform, - public readonly accessory: PlatformAccessory + public readonly accessory: PlatformAccessory ) { if (!this.platform.TeslaFleetApi?.vehicle) { throw new Error("TeslaFleetApi not initialized"); @@ -30,24 +49,13 @@ export class VehicleAccessory { this.emitter = new EventEmitter(); - this.information = this.accessory.getService( - this.platform.Service.AccessoryInformation - )!; - - this.information - .setCharacteristic(this.platform.Characteristic.Manufacturer, "Tesla") - .setCharacteristic(this.platform.Characteristic.Model, this.vehicle.model) - .setCharacteristic( - this.platform.Characteristic.SerialNumber, - this.vehicle.vin - ); - this.refresh(); setInterval(() => this.refresh(), REFRESH_INTERVAL); // each service must implement at-minimum the "required characteristics" for the given service type // see https://developers.homebridge.io/#/service/Lightbulb. + new AccessoryInformationService(this); new BatteryService(this); } @@ -61,25 +69,35 @@ export class VehicleAccessory { "vehicle_state", ]) .then(({ charge_state, climate_state, drive_state, vehicle_state }) => { - this.accessory.context.data = { - charge_state, - climate_state, - drive_state, - vehicle_state, - }; - this.emitter.emit("vehicle_data", this.accessory.context.data); - - this.information.updateCharacteristic( - this.platform.Characteristic.Active, - true - ); + this.accessory.context.state = "online"; + this.accessory.context.charge_state = charge_state; + this.accessory.context.climate_state = climate_state; + this.accessory.context.drive_state = drive_state; + this.accessory.context.vehicle_state = vehicle_state; + this.emitter.emit("vehicle_data", this.accessory.context); }) .catch((error: string) => { this.platform.log.warn(error); - this.information.updateCharacteristic( - this.platform.Characteristic.Active, - false - ); + this.accessory.context.state = "offline"; }); } + + async wake_up() { + if (this.accessory.context.state === "online") { + return Promise.resolve(); + } + await this.vehicle.wake_up(); + + let interval = 2000; + for (let x = 0; x < 5; x++) { + await new Promise((resolve) => setTimeout(resolve, interval)); + const { state } = await this.vehicle.vehicle(); + this.accessory.context.state = state; + if (state === "online") { + return Promise.resolve(); + } + interval = interval + 2000; + } + return Promise.reject("Vehicle didn't wake up"); + } }