From b8434460bc6b0c0b6c5ca4db32e77fa4132c428a Mon Sep 17 00:00:00 2001 From: Dennis Blommesteijn Date: Tue, 11 Apr 2023 20:46:28 +0200 Subject: [PATCH] Cleanup; Add accessory config; Add internal temperature --- .gitignore | 4 +- config.schema.json | 18 ++++ package-lock.json | 32 ++++++++ package.json | 7 +- src/electricityWattAccessory.ts | 44 ++++++++++ src/index.ts | 4 +- src/platform.ts | 116 +++++++++++++++++--------- src/platformAccessory.ts | 140 -------------------------------- src/powerStationAccessory.ts | 3 + src/temperatureAccessory.ts | 41 ++++++++++ 10 files changed, 226 insertions(+), 183 deletions(-) create mode 100644 src/electricityWattAccessory.ts delete mode 100644 src/platformAccessory.ts create mode 100644 src/powerStationAccessory.ts create mode 100644 src/temperatureAccessory.ts diff --git a/.gitignore b/.gitignore index 761da75..9feb5b4 100644 --- a/.gitignore +++ b/.gitignore @@ -117,4 +117,6 @@ dist .yarn/cache .yarn/unplugged .yarn/build-state.yml -.pnp.* \ No newline at end of file +.pnp.* + +*.DS_Store diff --git a/config.schema.json b/config.schema.json index 38a06fd..7ada5f9 100644 --- a/config.schema.json +++ b/config.schema.json @@ -32,6 +32,24 @@ "description": "UUIID found in: https://www.semsportal.com/PowerStation/PowerStatusSnMin/", "type": "string" } + }, + "showCurrentPowerLevel": { + "title": "Show current power level (Watt)", + "type": "boolean", + "required": true, + "default": true + }, + "showDayTotal": { + "title": "Show total Day generation (Watt)", + "type": "boolean", + "required": true, + "default": true + }, + "showInternalTemperature": { + "title": "Show Internal Temperature (degrees C)", + "type": "boolean", + "required": true, + "default": true } } } diff --git a/package-lock.json b/package-lock.json index 6853402..30f3b5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { + "dig-ts": "^1.0.8", "request": "^2.88.2", "util": "^0.12.4" }, @@ -839,6 +840,22 @@ "node": ">=0.3.1" } }, + "node_modules/dig-ts": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/dig-ts/-/dig-ts-1.0.8.tgz", + "integrity": "sha512-e+0cTYN8VLkqWyrchZE0Qljmrgp81SaH26t89/VuAE6RST015mWIeQz4fvF9YCi6IhZAGUJNVOdho9uBH1tGOg==", + "dependencies": { + "@types/node": "^10.12.9" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dig-ts/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3816,6 +3833,21 @@ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, + "dig-ts": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/dig-ts/-/dig-ts-1.0.8.tgz", + "integrity": "sha512-e+0cTYN8VLkqWyrchZE0Qljmrgp81SaH26t89/VuAE6RST015mWIeQz4fvF9YCi6IhZAGUJNVOdho9uBH1tGOg==", + "requires": { + "@types/node": "^10.12.9" + }, + "dependencies": { + "@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + } + } + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", diff --git a/package.json b/package.json index 48c6b7a..230b120 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "private": true, + "private": false, "displayName": "Homebridge SEMS", "name": "homebridge-plusems", "version": "0.0.1", - "description": "This plugin consumes the API from SEMS portal for GoodWE inverters", + "description": "This plugin consumes the API from SEMS portal for GoodWE inverters (for solar power)", "license": "Apache-2.0", "repository": { "type": "git", @@ -28,7 +28,8 @@ ], "dependencies": { "request": "^2.88.2", - "util": "^0.12.4" + "util": "^0.12.4", + "dig-ts": ">= 2.9" }, "devDependencies": { "@types/node": "^16.10.9", diff --git a/src/electricityWattAccessory.ts b/src/electricityWattAccessory.ts new file mode 100644 index 0000000..c1d997e --- /dev/null +++ b/src/electricityWattAccessory.ts @@ -0,0 +1,44 @@ +import { Service, PlatformAccessory } from 'homebridge'; +import { HomebridgeSems } from './platform'; +import { PowerStationAccessory } from './powerStationAccessory'; +import { dig } from 'dig-ts'; + + +export class ElectricityWattAccessory extends PowerStationAccessory { + private service: Service; + + constructor( + private readonly platform: HomebridgeSems, + private readonly accessory: PlatformAccessory, + ) { + super(); + // set accessory information TODO: add model and serialnumber! + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, 'GoodWe') + .setCharacteristic(this.platform.Characteristic.Model, 'model') + .setCharacteristic(this.platform.Characteristic.SerialNumber, 'sn'); + + this.service = this.accessory.getService(this.platform.Service.LightSensor) || + this.accessory.addService(this.platform.Service.LightSensor); + this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.name); + + this.update(accessory.context.device.data); + } + + async update(powerStationData) { + let value = dig(powerStationData, this.accessory.context.device.dataDigPath).get() as number; + const multiplier = this.accessory.context.device.multiplier; + if(multiplier) { + value = value * multiplier; + } + this.setValue(value); + } + + setValue(value) { + this.platform.log.info('New value', this.accessory.context.device.name, value); + if(value < 0.0001) { + value = 0.0001; + } + this.service.getCharacteristic(this.platform.Characteristic.CurrentAmbientLightLevel).updateValue(value); + } +} diff --git a/src/index.ts b/src/index.ts index b7a4fee..f8e9725 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ import { API } from 'homebridge'; import { PLATFORM_NAME } from './settings'; -import { HomeBridgeSems } from './platform'; +import { HomebridgeSems } from './platform'; /** * This method registers the platform with Homebridge */ export = (api: API) => { - api.registerPlatform(PLATFORM_NAME, HomeBridgeSems); + api.registerPlatform(PLATFORM_NAME, HomebridgeSems); }; diff --git a/src/platform.ts b/src/platform.ts index 68ab3d9..edab052 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -5,14 +5,12 @@ import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; import request from 'request'; import util from 'util'; +import { PowerStationAccessory } from './powerStationAccessory'; +import { ElectricityWattAccessory } from './electricityWattAccessory'; +import { TemperatureAccessory } from './temperatureAccessory'; const requestPromise = util.promisify(request); -/** - * HomebridgePlatform - * This class is the main constructor for your plugin, this is where you should - * parse the user config and discover/register accessories with Homebridge. - */ -export class HomeBridgeSems implements DynamicPlatformPlugin { +export class HomebridgeSems implements DynamicPlatformPlugin { public readonly Service: typeof Service = this.api.hap.Service; public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic; @@ -28,46 +26,98 @@ export class HomeBridgeSems implements DynamicPlatformPlugin { public readonly api: API, ) { this.log.debug('Finished initializing platform:', this.config.name); - - // When this event is fired it means Homebridge has restored all cached accessories from disk. - // Dynamic Platform plugins should only register new accessories after this event was fired, - // in order to ensure they weren't added to homebridge already. This event can also be used - // to start discovery of new accessories. this.api.on('didFinishLaunching', () => { log.debug('Executed didFinishLaunching callback'); - // run the method to discover / register your devices as accessories this.discoverDevices(); }); } - /** - * This function is invoked when homebridge restores cached accessories from disk at startup. - * It should be used to setup event handlers for characteristics and update respective values. - */ 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 this.accessories.push(accessory); } - /** - * This is an example method showing how to register discovered accessories. - * Accessories must only be registered once, previously created accessories - * must not be registered again to prevent "duplicate UUID" errors. - */ + loadElectricityAccessory(powerStationId, powerStationData, dataDigPath, name, multiplier = 1): ElectricityWattAccessory { + const uuid = this.api.hap.uuid.generate(`power_station_${powerStationId}_${dataDigPath.join()}`); + const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); + let ret; + + if(existingAccessory) { + this.log.info('found cached accessory', existingAccessory.displayName); + ret = new ElectricityWattAccessory(this, existingAccessory); + } else { + this.log.info('found new accessory', name); + const accessory = new this.api.platformAccessory(name, uuid); + accessory.context.device = { data: powerStationData, dataDigPath: dataDigPath, id: powerStationId, name: name, + multiplier: multiplier }; + ret = new ElectricityWattAccessory(this, accessory); + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + } + return ret; + } + + loadTemperatureAccessory(powerStationId, powerStationData, dataDigPath, name, multiplier = 1): ElectricityWattAccessory { + const uuid = this.api.hap.uuid.generate(`power_station_${powerStationId}_${dataDigPath.join()}`); + const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); + let ret; + + if(existingAccessory) { + this.log.info('found cached accessory', existingAccessory.displayName); + ret = new TemperatureAccessory(this, existingAccessory); + } else { + this.log.info('found new accessory', name); + const accessory = new this.api.platformAccessory(name, uuid); + accessory.context.device = { data: powerStationData, dataDigPath: dataDigPath, id: powerStationId, name: name, + multiplier: multiplier }; + ret = new TemperatureAccessory(this, accessory); + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + } + return ret; + } + async discoverDevices() { - this.log.debug('test'); + // iterate all configured power-stations for(const powerStationId of this.config.powerStationIds){ + // load power-station-data form API const powerStationData = await this.getPowerStationDataById(powerStationId); - this.log.debug(`powerStationData for: ${powerStationId}`, powerStationData); - - // TODO: implement current incoming: powerStationData.soc.power for generated in kW?a - // TODO: implement total per day: powerStationData.kpi.power for day total in kW + this.log.debug('powerStationData for:', powerStationId); + const accessories: PowerStationAccessory[] = []; + + if(this.config.showCurrentPowerLevel) { + accessories.push( + this.loadElectricityAccessory(powerStationId, powerStationData, ['kpi', 'pac'], 'Current Production W', 1)); + } + + if(this.config.showDayTotal) { + accessories.push( + this.loadElectricityAccessory(powerStationId, powerStationData, ['kpi', 'power'], 'Day Production Wh', 1000)); + } + + if(this.config.showInternalTemperature) { + // NOTE: tempperature is not a typo :-D + accessories.push( + this.loadTemperatureAccessory(powerStationId, powerStationData, ['inverter', 0, 'tempperature'], 'Internal Temperature', 1)); + } + + // update all accessories with new data + this.fetchPowerStationUpdate(5000, powerStationId, accessories); } } + async fetchPowerStationUpdate(timeout: number, powerStationId, accessories) { + setInterval(async () => { + const powerStationData = await this.getPowerStationDataById(powerStationId); + this.log.debug('fetchPowerStationUpdate', powerStationId, accessories.length); + for(const accessory of accessories) { + accessory.update(powerStationData); + } + }, timeout); + } + async login() { + if(this.loginResponseBody.data) { + return; + } this.loginAttempts++; const json = { account: this.config.email, pwd: this.config.password }; @@ -78,7 +128,6 @@ export class HomeBridgeSems implements DynamicPlatformPlugin { this.log.debug('login response: ', response.statusCode); if(response.statusCode === 200) { - this.log.debug('login response: ', response.body); this.loginResponseBody = response.body; this.log.info('Login successful'); } else { @@ -104,7 +153,7 @@ export class HomeBridgeSems implements DynamicPlatformPlugin { await this.clearAuthenticationAndSleepAfterTooManyAttempts(); return await this.getPowerStationDataById(powerStationId); } else { - this.log.debug('data response: ', response.statusCode, response.body); + // this.log.debug('data response: ', response.statusCode, response.body); } return response.body.data; } @@ -128,11 +177,4 @@ export class HomeBridgeSems implements DynamicPlatformPlugin { }); } } - - async haltOnConfigError(errorMessage) { - this.log.info(`Configuration error: ${errorMessage}`); - await this.sleep(() => { - this.log.debug('sleeping...'); - }); - } } diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts deleted file mode 100644 index c29e90d..0000000 --- a/src/platformAccessory.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; -import { HomeBridgeSems } from './platform'; - -/** - * Platform Accessory - * An instance of this class is created for each accessory your platform registers - * Each accessory may expose multiple services of different service types. - */ -export class ExamplePlatformAccessory { - private service: Service; - - /** - * 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, - }; - - constructor( - private readonly platform: HomeBridgeSems, - private readonly accessory: PlatformAccessory, - ) { - - // set accessory information - this.accessory.getService(this.platform.Service.AccessoryInformation)! - .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Default-Manufacturer') - .setCharacteristic(this.platform.Characteristic.Model, 'Default-Model') - .setCharacteristic(this.platform.Characteristic.SerialNumber, 'Default-Serial'); - - // 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, accessory.context.device.exampleDisplayName); - - // 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 - - // register handlers for the Brightness Characteristic - this.service.getCharacteristic(this.platform.Characteristic.Brightness) - .onSet(this.setBrightness.bind(this)); // SET - bind to the 'setBrightness` method below - - /** - * 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 sub type 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); - } - - /** - * 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; - - 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 possbile. 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); - } - -} diff --git a/src/powerStationAccessory.ts b/src/powerStationAccessory.ts new file mode 100644 index 0000000..e8ca99c --- /dev/null +++ b/src/powerStationAccessory.ts @@ -0,0 +1,3 @@ + +export class PowerStationAccessory { +} diff --git a/src/temperatureAccessory.ts b/src/temperatureAccessory.ts new file mode 100644 index 0000000..161114b --- /dev/null +++ b/src/temperatureAccessory.ts @@ -0,0 +1,41 @@ +import { Service, PlatformAccessory } from 'homebridge'; +import { HomebridgeSems } from './platform'; +import { PowerStationAccessory } from './powerStationAccessory'; +import { dig } from 'dig-ts'; + + +export class TemperatureAccessory extends PowerStationAccessory { + private service: Service; + + constructor( + private readonly platform: HomebridgeSems, + private readonly accessory: PlatformAccessory, + ) { + super(); + // set accessory information TODO: add model and serialnumber! + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, 'GoodWe') + .setCharacteristic(this.platform.Characteristic.Model, 'model') + .setCharacteristic(this.platform.Characteristic.SerialNumber, 'sn'); + + this.service = this.accessory.getService(this.platform.Service.TemperatureSensor) || + this.accessory.addService(this.platform.Service.TemperatureSensor); + this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.name); + + this.update(accessory.context.device.data); + } + + async update(powerStationData) { + let value = dig(powerStationData, this.accessory.context.device.dataDigPath).get() as number; + const multiplier = this.accessory.context.device.multiplier; + if(multiplier) { + value = value * multiplier; + } + this.setValue(value); + } + + setValue(value) { + this.platform.log.info('New value', this.accessory.context.device.name, value); + this.service.getCharacteristic(this.platform.Characteristic.CurrentTemperature).updateValue(value); + } +}