diff --git a/package-lock.json b/package-lock.json index 693ac53..e157aec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { - "tesla-fleet-api": "^0.0.10" + "tesla-fleet-api": "^0.0.15" }, "devDependencies": { "@types/node": "^20.12.13", @@ -3139,9 +3139,9 @@ } }, "node_modules/tesla-fleet-api": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/tesla-fleet-api/-/tesla-fleet-api-0.0.10.tgz", - "integrity": "sha512-2l51RoarovxsG5tUokuvHIweJ/Ie+GqNLJfYphEUQxnmKxFdF+UmmHgo37d8n9Pp2zbd5inMwTwLSvl1jH67cg==" + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/tesla-fleet-api/-/tesla-fleet-api-0.0.15.tgz", + "integrity": "sha512-d09Cw4GLxqegjo2GKAn0HBwKKIbkI2vkTNJtG2HLoLH5542WTYRoVp2JjeBRIg4yPKlpOTAIh9F9wyCpQ2hxZA==" }, "node_modules/text-table": { "version": "0.2.0", diff --git a/package.json b/package.json index 638b431..b239dfa 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,6 @@ "typescript": "^5.4.5" }, "dependencies": { - "tesla-fleet-api": "^0.0.10" + "tesla-fleet-api": "^0.0.15" } } diff --git a/src/events.ts b/src/events.ts new file mode 100644 index 0000000..f9244be --- /dev/null +++ b/src/events.ts @@ -0,0 +1,26 @@ +type Listener = (...args: any[]) => void; + +type ListenerArgs = T extends Listener ? Parameters : never; + +export class EventEmitter { + private events = new Map(); // Can't really make this typesafe. + + public on(type: T, listener: E[T]) { + const { events } = this; + const listeners = events.get(type); + + if (listeners) { + listeners.add(listener); + } else { + events.set(type, new Set([listener])); + } + } + + public off(type: T, listener: E[T]) { + this.events.get(type)?.delete(listener); + } + + public emit(type: T, ...args: ListenerArgs) { + this.events.get(type)?.forEach((listener: Listener) => listener(...args)); + } +} diff --git a/src/platform.ts b/src/platform.ts index 5204715..cacff84 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -72,42 +72,41 @@ export class TeslaFleetApiPlatform implements DynamicPlatformPlugin { * must not be registered again to prevent "duplicate UUID" errors. */ discoverDevices() { - this.TeslaFleetApi.products().then((products: any) => { - for (const product of products.response) { - // Vehicles - if ("vin" in product) { - const uuid = this.api.hap.uuid.generate(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.info( - "Restoring existing accessory from cache:", - cachedAccessory.displayName - ); - new VehicleAccessory(this, cachedAccessory); - continue; - } - - this.log.info("Adding new accessory:", product.display_name); - const newAccessory = new this.api.platformAccessory( - product.display_name, - uuid + this.TeslaFleetApi.products_by_type().then(({ vehicles, energy_sites }) => { + vehicles.forEach((product) => { + const uuid = this.api.hap.uuid.generate(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.info( + "Restoring existing accessory from cache:", + cachedAccessory.displayName ); + new VehicleAccessory(this, cachedAccessory); + return; + } - newAccessory.context.vin = product.vin; - newAccessory.context.state = product.state; - newAccessory.displayName = product.display_name; + this.log.info("Adding new accessory:", product.display_name); + const newAccessory = new this.api.platformAccessory( + product.display_name, + uuid + ); - new VehicleAccessory(this, newAccessory); + newAccessory.context.vin = product.vin; + newAccessory.context.state = product.state; + newAccessory.displayName = product.display_name; - this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [ - newAccessory, - ]); - } - } + new VehicleAccessory(this, newAccessory); + + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [ + newAccessory, + ]); + }); + + energy_sites.forEach((product) => {}); }); } } diff --git a/src/services/battery.ts b/src/services/battery.ts new file mode 100644 index 0000000..ff5a333 --- /dev/null +++ b/src/services/battery.ts @@ -0,0 +1,52 @@ +import { PlatformAccessory, Service } from "homebridge"; +import { VehicleSpecific } from "tesla-fleet-api"; +import { TeslaFleetApiPlatform } from "../platform.js"; + +export class BatteryService { + service: Service; + + constructor( + private platform: TeslaFleetApiPlatform, + private accessory: PlatformAccessory, + private vehicle: VehicleSpecific + ) { + this.service = + this.accessory.getService(this.platform.Service.BatteryService) || + this.accessory.addService(this.platform.Service.BatteryService); + + const batteryLevel = this.service + .getCharacteristic(this.platform.Characteristic.BatteryLevel) + .onGet(this.getLevel.bind(this)); + + const chargingState = this.service + .getCharacteristic(this.platform.Characteristic.ChargingState) + .onGet(this.getChargingState.bind(this)); + + const lowBattery = this.service + .getCharacteristic(this.platform.Characteristic.StatusLowBattery) + .onGet(this.getLowBattery.bind(this)); + + /*tesla.on("vehicleDataUpdated", (data) => { + batteryLevel.updateValue(this.getLevel(data)); + chargingState.updateValue(this.getChargingState(data)); + lowBattery.updateValue(this.getLowBattery(data)); + });*/ + } + + getLevel(): number { + // Assume 50% when not connected and no last-known state. + return this.accessory.context?.charge_state?.battery_level ?? 0; + } + + getChargingState(): number { + return this.accessory.context?.charge_state?.charging_state === "Charging" + ? this.platform.Characteristic.ChargingState.CHARGING + : this.platform.Characteristic.ChargingState.NOT_CHARGING; + } + + getLowBattery(): boolean { + return this.accessory.context?.charge_state?.battery_level + ? this.accessory.context.charge_state.battery_level <= 20 + : false; + } +} diff --git a/src/services/vehicle.ts b/src/services/vehicle.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/vehicle.ts b/src/vehicle.ts index 3bd52a4..b62556d 100644 --- a/src/vehicle.ts +++ b/src/vehicle.ts @@ -2,6 +2,7 @@ import { CharacteristicValue, PlatformAccessory, Service } from "homebridge"; import VehicleSpecific from "tesla-fleet-api/dist/vehiclespecific.js"; import { TeslaFleetApiPlatform } from "./platform.js"; +import { BatteryService } from "./services/battery.js"; import { REFRESH_INTERVAL } from "./settings.js"; /** @@ -10,16 +11,8 @@ import { REFRESH_INTERVAL } from "./settings.js"; * Each accessory may expose multiple services of different service types. */ export class VehicleAccessory { - private service: Service; - private VehicleSpecific: VehicleSpecific; - /** - * These are just used to create a working example - * You should implement your own code to track the state of your accessory - */ - private exampleStates = { - On: false, - Brightness: 100, - }; + private vehicle: VehicleSpecific; + private information: Service; constructor( private readonly platform: TeslaFleetApiPlatform, @@ -29,188 +22,58 @@ export class VehicleAccessory { throw new Error("TeslaFleetApi not initialized"); } - this.VehicleSpecific = this.platform.TeslaFleetApi.vehicle.specific( + this.vehicle = this.platform.TeslaFleetApi.vehicle.specific( this.accessory.context.vin ); - this.accessory - .getService(this.platform.Service.AccessoryInformation)! + this.information = this.accessory.getService( + this.platform.Service.AccessoryInformation + )!; + + this.information .setCharacteristic(this.platform.Characteristic.Manufacturer, "Tesla") - .setCharacteristic( - this.platform.Characteristic.Model, - this.VehicleSpecific.model - ) + .setCharacteristic(this.platform.Characteristic.Model, this.vehicle.model) .setCharacteristic( this.platform.Characteristic.SerialNumber, - this.accessory.context.vin + this.vehicle.vin ); this.refresh(); setInterval(() => this.refresh(), REFRESH_INTERVAL); - // get the LightBulb service if it exists, otherwise create a new LightBulb service - // you can create multiple services for each accessory - this.service = - this.accessory.getService(this.platform.Service.Lightbulb) || - this.accessory.addService(this.platform.Service.Lightbulb); - - // set the service name, this is what is displayed as the default name on the Home app - // in this example we are using the name we stored in the `accessory.context` in the `discoverDevices` method. - this.service.setCharacteristic( - this.platform.Characteristic.Name, - "Flash Lights" - ); - // each service must implement at-minimum the "required characteristics" for the given service type - // see https://developers.homebridge.io/#/service/Lightbulb - - // register handlers for the On/Off Characteristic - this.service - .getCharacteristic(this.platform.Characteristic.On) - .onSet(this.setOn.bind(this)) // SET - bind to the `setOn` method below - .onGet(this.getOn.bind(this)); // GET - bind to the `getOn` method below + // see https://developers.homebridge.io/#/service/Lightbulb. - /** - * Creating multiple services of the same type. - * - * To avoid "Cannot add a Service with the same UUID another Service without also defining a unique 'subtype' property." error, - * when creating multiple services of the same type, you need to use the following syntax to specify a name and subtype id: - * this.accessory.getService('NAME') || this.accessory.addService(this.platform.Service.Lightbulb, 'NAME', 'USER_DEFINED_SUBTYPE_ID'); - * - * The USER_DEFINED_SUBTYPE must be unique to the platform accessory (if you platform exposes multiple accessories, each accessory - * can use the same subtype id.) - */ - - // Example: add two "motion sensor" services to the accessory - /*const motionSensorOneService = - this.accessory.getService("Motion Sensor One Name") || - this.accessory.addService( - this.platform.Service.MotionSensor, - "Motion Sensor One Name", - "YourUniqueIdentifier-1" - ); - - const motionSensorTwoService = - this.accessory.getService("Motion Sensor Two Name") || - this.accessory.addService( - this.platform.Service.MotionSensor, - "Motion Sensor Two Name", - "YourUniqueIdentifier-2" - );*/ - - /** - * Updating characteristics values asynchronously. - * - * Example showing how to update the state of a Characteristic asynchronously instead - * of using the `on('get')` handlers. - * Here we change update the motion sensor trigger states on and off every 10 seconds - * the `updateCharacteristic` method. - * - */ - /*let motionDetected = false; - setInterval(() => { - // EXAMPLE - inverse the trigger - motionDetected = !motionDetected; - - // push the new value to HomeKit - motionSensorOneService.updateCharacteristic( - this.platform.Characteristic.MotionDetected, - motionDetected - ); - motionSensorTwoService.updateCharacteristic( - this.platform.Characteristic.MotionDetected, - !motionDetected - ); - - this.platform.log.debug( - "Triggering motionSensorOneService:", - motionDetected - ); - this.platform.log.debug( - "Triggering motionSensorTwoService:", - !motionDetected - ); - }, 10000);*/ + new BatteryService(this.platform, this.accessory, this.vehicle); } async refresh() { - this.VehicleSpecific.vehicle_data([ - "charge_state", - "climate_state", - "drive_state", - "location_data", - "vehicle_state", - ]) - .then( - ({ - response: { charge_state, climate_state, drive_state, vehicle_state }, - }) => { - this.accessory.context.data = { - charge_state, - climate_state, - drive_state, - vehicle_state, - }; - this.service.updateCharacteristic( - this.platform.Characteristic.Active, - true - ); - } - ) - .catch((error) => { + this.vehicle + .vehicle_data([ + "charge_state", + "climate_state", + "drive_state", + "location_data", + "vehicle_state", + ]) + .then(({ charge_state, climate_state, drive_state, vehicle_state }) => { + this.accessory.context.data = { + charge_state, + climate_state, + drive_state, + vehicle_state, + }; + this.information.updateCharacteristic( + this.platform.Characteristic.Active, + true + ); + }) + .catch((error: string) => { this.platform.log.warn(error); - this.service.updateCharacteristic( + this.information.updateCharacteristic( this.platform.Characteristic.Active, false ); }); } - - /** - * Handle "SET" requests from HomeKit - * These are sent when the user changes the state of an accessory, for example, turning on a Light bulb. - */ - async setOn(value: CharacteristicValue) { - // implement your own code to turn your device on/off - this.exampleStates.On = value as boolean; - await this.VehicleSpecific.flash_lights(); - - this.platform.log.debug("Set Characteristic On ->", value); - } - - /** - * Handle the "GET" requests from HomeKit - * These are sent when HomeKit wants to know the current state of the accessory, for example, checking if a Light bulb is on. - * - * GET requests should return as fast as possible. A long delay here will result in - * HomeKit being unresponsive and a bad user experience in general. - * - * If your device takes time to respond you should update the status of your device - * asynchronously instead using the `updateCharacteristic` method instead. - - * @example - * this.service.updateCharacteristic(this.platform.Characteristic.On, true) - */ - async getOn(): Promise { - // implement your own code to check if the device is on - const isOn = this.exampleStates.On; - - this.platform.log.debug("Get Characteristic On ->", isOn); - - // if you need to return an error to show the device as "Not Responding" in the Home app: - // throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE); - - return isOn; - } - - /** - * Handle "SET" requests from HomeKit - * These are sent when the user changes the state of an accessory, for example, changing the Brightness - */ - async setBrightness(value: CharacteristicValue) { - // implement your own code to set the brightness - this.exampleStates.Brightness = value as number; - - this.platform.log.debug("Set Characteristic Brightness -> ", value); - } }