From 296f8b3336ef851fa1e1bdff67c66640615ddb63 Mon Sep 17 00:00:00 2001 From: Tomek Kleszcz Date: Sun, 10 Mar 2024 15:32:46 +0100 Subject: [PATCH 01/11] fix: get api key and regional base url before logging in to gigya (#47) --- config.schema.json | 32 -------------------- src/const/apiKey.ts | 5 ++-- src/definitions/region.ts | 1 - src/platform.ts | 63 ++++++++++++++++++++++++++------------- 4 files changed, 45 insertions(+), 56 deletions(-) delete mode 100644 src/definitions/region.ts diff --git a/config.schema.json b/config.schema.json index 13003f1..c7b0b58 100644 --- a/config.schema.json +++ b/config.schema.json @@ -17,38 +17,6 @@ "required": true, "default": "" }, - "region": { - "title": "Region", - "type": "string", - "required": true, - "default": "eu", - "oneOf": [ - { - "title": "Europe", - "enum": ["eu"] - }, - { - "title": "United States", - "enum": ["us"] - }, - { - "title": "Australia", - "enum": ["au"] - }, - { - "title": "Russia", - "enum": ["ru"] - }, - { - "title": "China", - "enum": ["cn"] - }, - { - "title": "Israel", - "enum": ["il"] - } - ] - }, "pollingInterval": { "title": "Polling interval", "type": "number", diff --git a/src/const/apiKey.ts b/src/const/apiKey.ts index 8d4f48b..c34e52e 100644 --- a/src/const/apiKey.ts +++ b/src/const/apiKey.ts @@ -1,3 +1,4 @@ -export const ACCOUNTS_API_KEY = '4_JZvZObbVWc1YROHF9e6y8A'; - +export const CLIENT_ID = 'ElxOneApp'; +export const CLIENT_SECRET = + '8UKrsKD7jH9zvTV7rz5HeCLkit67Mmj68FvRVTlYygwJYy4dW6KF2cVLPKeWzUQUd6KJMtTifFf4NkDnjI7ZLdfnwcPtTSNtYvbP7OzEkmQD9IjhMOf5e1zeAQYtt2yN'; export const API_KEY = '2AMqwEV5MqVhTKrRCyYfVF8gmKrd2rAmp7cUsfky'; diff --git a/src/definitions/region.ts b/src/definitions/region.ts deleted file mode 100644 index 9ec3712..0000000 --- a/src/definitions/region.ts +++ /dev/null @@ -1 +0,0 @@ -export type Region = 'eu' | 'us' | 'au' | 'ru' | 'cn' | 'il'; diff --git a/src/platform.ts b/src/platform.ts index cabf3ac..74f36c1 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -12,14 +12,13 @@ import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; import { axiosApi, axiosAppliance, axiosAuth } from './services/axios'; import { Appliances } from './definitions/appliances'; import { DEVICES } from './const/devices'; -import { ACCOUNTS_API_KEY } from './const/apiKey'; -import Gigya from 'gigya'; +import { CLIENT_ID, CLIENT_SECRET } from './const/apiKey'; +import Gigya, { DataCenter } from 'gigya'; import { TokenResponse } from './definitions/auth'; import { ElectroluxAccessoryController } from './accessories/controller'; import { ElectroluxAccessory } from './accessories/accessory'; import fs from 'fs'; import path from 'path'; -import { Region } from './definitions/region'; import { IdentityProvidersResponse } from './definitions/identityProviders'; import { API_URL } from './const/url'; @@ -102,9 +101,45 @@ export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin { } async signIn() { - const region: Region = this.config.region || 'eu'; + /* + Get the token from Electrolux API using CLIENT_ID and CLIENT_SECRET + to fetch the regional base URL and API key. + */ + const tokenResponse = await axiosAuth.post( + '/one-account-authorization/api/v1/token', + { + grantType: 'client_credentials', + clientId: CLIENT_ID, + clientSecret: CLIENT_SECRET, + scope: '' + }, + { + baseURL: API_URL + } + ); - this.gigya = new Gigya(ACCOUNTS_API_KEY, `${region}1`); + const regionResponse = await axiosApi.get( + '/one-account-user/api/v1/identity-providers', + { + headers: { + Authorization: `Bearer ${tokenResponse.data.accessToken}` + } + } + ); + + const regionData = regionResponse.data.find( + ({ brand }) => brand === 'electrolux' + ); + if (!regionData) { + throw new Error('Region not found'); + } + + this.regionalBaseUrl = regionData?.httpRegionalBaseUrl ?? null; + + this.gigya = new Gigya( + regionData?.apiKey, + regionData.domain.split('.')[0] as DataCenter + ); const storagePath = path.format({ dir: this.api.user.storagePath(), @@ -164,7 +199,7 @@ export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin { { grantType: 'urn:ietf:params:oauth:grant-type:token-exchange', - clientId: 'ElxOneApp', + clientId: CLIENT_ID, idToken: jwtResponse.id_token, scope: '' }, @@ -184,20 +219,6 @@ export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin { this.log.info('JWT token successfully fetched!'); } - const regionResponse = - await axiosApi.get( - '/one-account-user/api/v1/identity-providers', - { - headers: { - Authorization: `Bearer ${this.accessToken}` - } - } - ); - - this.regionalBaseUrl = - regionResponse.data.find(({ brand }) => brand === 'electrolux') - ?.httpRegionalBaseUrl ?? null; - const json = JSON.stringify({ uid: this.uid, oauthToken: this.oauthToken, @@ -237,7 +258,7 @@ export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin { '/token', { grantType: 'refresh_token', - clientId: 'ElxOneApp', + clientId: CLIENT_ID, refreshToken: this.refreshToken, scope: '' }, From 43480b60bcfbd2cada1cc5c6164f0db0f02f2df0 Mon Sep 17 00:00:00 2001 From: tomekkleszcz Date: Sun, 10 Mar 2024 15:33:17 +0100 Subject: [PATCH 02/11] chore: 0.0.10 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index db53751..c463785 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "displayName": "Homebridge Electrolux Devices", "name": "homebridge-electrolux-devices", - "version": "0.0.9", + "version": "0.0.10", "description": "Homebridge plugin for Electrolux devices", "license": "Apache-2.0", "repository": { From 87743660eeb1928cc729821e1b2c70e3f2104200 Mon Sep 17 00:00:00 2001 From: Tomek Kleszcz Date: Sun, 10 Mar 2024 18:29:02 +0100 Subject: [PATCH 03/11] feat(air purifier): implement support for ultimate home 500 (#50) --- README.md | 12 +- .../devices/airPurifier/airPurifier.ts | 163 ------------------ src/accessories/devices/airPurifier/pureA9.ts | 163 ++++++++++++++++++ .../devices/airPurifier/ultimateHome500.ts | 88 ++++++++++ src/accessories/devices/airPurifier/wellA7.ts | 163 ++++++++++++++++++ src/const/devices.ts | 4 +- src/definitions/appliance.ts | 14 +- tsconfig.json | 40 ++--- 8 files changed, 448 insertions(+), 199 deletions(-) create mode 100644 src/accessories/devices/airPurifier/ultimateHome500.ts diff --git a/README.md b/README.md index 5148629..d7df309 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ -

- # Homebridge Electrolux Devices This is a plugin for connecting Electrolux devices which are controlled by the [Electrolux](https://apps.apple.com/pl/app/electrolux/id1595816832) app to Homekit. @@ -17,8 +15,10 @@ This is a plugin for connecting Electrolux devices which are controlled by the [ 3. Configure the plugin using the schema. ## 🌡️ Supported devices -- Comfort 600 air conditioner -- Well A7 air purifier -- Pure A9/AX 9 air purifier -If your device is not on the list, please create the issue. I'll be more than happy to implement the support for your device. 😄 \ No newline at end of file +- Comfort 600 air conditioner +- Well A7 air purifier +- Pure A9/AX 9 air purifier +- Electrolux UltimateHome 500 + +If your device is not on the list, please create the issue. I'll be more than happy to implement the support for your device. 😄 diff --git a/src/accessories/devices/airPurifier/airPurifier.ts b/src/accessories/devices/airPurifier/airPurifier.ts index 91932b3..b0672f1 100644 --- a/src/accessories/devices/airPurifier/airPurifier.ts +++ b/src/accessories/devices/airPurifier/airPurifier.ts @@ -3,14 +3,9 @@ import { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; import { ElectroluxDevicesPlatform } from '../../../platform'; import { Appliance, FilterType } from '../../../definitions/appliance'; import { ElectroluxAccessoryController } from '../../controller'; -import { tvocPPBToVocDensity } from '../../../util/voc'; export class AirPurifier extends ElectroluxAccessoryController { private airPurifierService: Service; - private ionizerService: Service; - private airQualityService: Service; - private humiditySensorService: Service; - private temperatureSensorService: Service; private particleFilterService?: Service; constructor( @@ -107,80 +102,6 @@ export class AirPurifier extends ElectroluxAccessoryController { ) ); - this.ionizerService = - this.accessory.getService(this.platform.Service.Switch) || - this.accessory.addService(this.platform.Service.Switch); - - this.ionizerService.setCharacteristic( - this.platform.Characteristic.Name, - 'Ionizer' - ); - - this.ionizerService - .getCharacteristic(this.platform.Characteristic.On) - .onGet(this.getCharacteristicValueGuard(this.getIonizer.bind(this))) - .onSet( - this.setCharacteristicValueGuard(this.setIonizer.bind(this)) - ); - - this.airQualityService = - this.accessory.getService(this.platform.Service.AirQualitySensor) || - this.accessory.addService(this.platform.Service.AirQualitySensor); - - this.airQualityService - .getCharacteristic(this.platform.Characteristic.AirQuality) - .onGet( - this.getCharacteristicValueGuard(this.getAirQuality.bind(this)) - ); - - this.airQualityService - .getCharacteristic(this.platform.Characteristic.PM2_5Density) - .onGet( - this.getCharacteristicValueGuard( - this.getPM2_5Density.bind(this) - ) - ); - - this.airQualityService - .getCharacteristic(this.platform.Characteristic.PM10Density) - .onGet( - this.getCharacteristicValueGuard(this.getPM10Density.bind(this)) - ); - - this.airQualityService - .getCharacteristic(this.platform.Characteristic.VOCDensity) - .onGet( - this.getCharacteristicValueGuard(this.getVOCDensity.bind(this)) - ); - - this.humiditySensorService = - this.accessory.getService(this.platform.Service.HumiditySensor) || - this.accessory.addService(this.platform.Service.HumiditySensor); - - this.humiditySensorService - .getCharacteristic( - this.platform.Characteristic.CurrentRelativeHumidity - ) - .onGet( - this.getCharacteristicValueGuard( - this.getCurrentRelativeHumidity.bind(this) - ) - ); - - this.temperatureSensorService = - this.accessory.getService( - this.platform.Service.TemperatureSensor - ) || - this.accessory.addService(this.platform.Service.TemperatureSensor); - - this.temperatureSensorService - .getCharacteristic(this.platform.Characteristic.CurrentTemperature) - .onGet( - this.getCharacteristicValueGuard( - this.getCurrentTemperature.bind(this) - ) - ); - if ( this.appliance.properties.reported.FilterType_1 === FilterType.ParticleFilter || @@ -378,58 +299,6 @@ export class AirPurifier extends ElectroluxAccessoryController { this.appliance.properties.reported.Fanspeed = value as number; } - async getIonizer(): Promise { - return this.appliance.properties.reported.Ionizer; - } - - async setIonizer(value: CharacteristicValue) { - await this.sendCommand({ - Ionizer: value - }); - - this.appliance.properties.reported.Ionizer = value as boolean; - } - - async getAirQuality(): Promise { - if (this.appliance.properties.reported.PM2_5 <= 25) { - return this.platform.Characteristic.AirQuality.EXCELLENT; - } else if (this.appliance.properties.reported.PM2_5 <= 50) { - return this.platform.Characteristic.AirQuality.GOOD; - } else if (this.appliance.properties.reported.PM2_5 <= 75) { - return this.platform.Characteristic.AirQuality.FAIR; - } else if (this.appliance.properties.reported.PM2_5 <= 100) { - return this.platform.Characteristic.AirQuality.INFERIOR; - } else { - return this.platform.Characteristic.AirQuality.POOR; - } - } - - async getPM2_5Density(): Promise { - return this.appliance.properties.reported.PM2_5; - } - - async getPM10Density(): Promise { - return this.appliance.properties.reported.PM10; - } - - async getVOCDensity(): Promise { - const vocDensity = tvocPPBToVocDensity( - this.appliance.properties.reported.TVOC, - this.appliance.properties.reported.Temp, - this._platform.config.vocMolecularWeight ?? 30.026 - ); - - return vocDensity; - } - - async getCurrentRelativeHumidity(): Promise { - return this.appliance.properties.reported.Humidity; - } - - async getCurrentTemperature(): Promise { - return this.appliance.properties.reported.Temp; - } - async getParticleFilterChangeIndication(): Promise { const filterLife = this.appliance.properties.reported.FilterType_1 === @@ -513,38 +382,6 @@ export class AirPurifier extends ElectroluxAccessoryController { this.appliance.properties.reported.Fanspeed ); - this.ionizerService.updateCharacteristic( - this.platform.Characteristic.On, - this.appliance.properties.reported.Ionizer ? 1 : 0 - ); - - this.airQualityService.updateCharacteristic( - this.platform.Characteristic.AirQuality, - await this.getAirQuality() - ); - this.airQualityService.updateCharacteristic( - this.platform.Characteristic.PM2_5Density, - this.appliance.properties.reported.PM2_5 - ); - this.airQualityService.updateCharacteristic( - this.platform.Characteristic.PM10Density, - this.appliance.properties.reported.PM10 - ); - this.airQualityService.updateCharacteristic( - this.platform.Characteristic.VOCDensity, - this.appliance.properties.reported.TVOC - ); - - this.humiditySensorService.updateCharacteristic( - this.platform.Characteristic.CurrentRelativeHumidity, - this.appliance.properties.reported.Humidity - ); - - this.temperatureSensorService.updateCharacteristic( - this.platform.Characteristic.CurrentTemperature, - this.appliance.properties.reported.Temp - ); - const filterLife = this.appliance.properties.reported.FilterType_1 === FilterType.ParticleFilter diff --git a/src/accessories/devices/airPurifier/pureA9.ts b/src/accessories/devices/airPurifier/pureA9.ts index beaef51..a2fd0e5 100644 --- a/src/accessories/devices/airPurifier/pureA9.ts +++ b/src/accessories/devices/airPurifier/pureA9.ts @@ -3,8 +3,13 @@ import { AirPurifier } from './airPurifier'; import { ElectroluxDevicesPlatform } from '../../../platform'; import { ElectroluxAccessoryController } from '../../controller'; import { Appliance } from '../../../definitions/appliance'; +import { tvocPPBToVocDensity } from '../../../util/voc'; export class PureA9 extends AirPurifier { + private ionizerService: Service; + private airQualityService: Service; + private humiditySensorService: Service; + private temperatureSensorService: Service; private carbonDioxideSensorService: Service; constructor( @@ -14,6 +19,80 @@ export class PureA9 extends AirPurifier { ) { super(_platform, _accessory, _appliance); + this.ionizerService = + this.accessory.getService(this.platform.Service.Switch) || + this.accessory.addService(this.platform.Service.Switch); + + this.ionizerService.setCharacteristic( + this.platform.Characteristic.Name, + 'Ionizer' + ); + + this.ionizerService + .getCharacteristic(this.platform.Characteristic.On) + .onGet(this.getCharacteristicValueGuard(this.getIonizer.bind(this))) + .onSet( + this.setCharacteristicValueGuard(this.setIonizer.bind(this)) + ); + + this.airQualityService = + this.accessory.getService(this.platform.Service.AirQualitySensor) || + this.accessory.addService(this.platform.Service.AirQualitySensor); + + this.airQualityService + .getCharacteristic(this.platform.Characteristic.AirQuality) + .onGet( + this.getCharacteristicValueGuard(this.getAirQuality.bind(this)) + ); + + this.airQualityService + .getCharacteristic(this.platform.Characteristic.PM2_5Density) + .onGet( + this.getCharacteristicValueGuard( + this.getPM2_5Density.bind(this) + ) + ); + + this.airQualityService + .getCharacteristic(this.platform.Characteristic.PM10Density) + .onGet( + this.getCharacteristicValueGuard(this.getPM10Density.bind(this)) + ); + + this.airQualityService + .getCharacteristic(this.platform.Characteristic.VOCDensity) + .onGet( + this.getCharacteristicValueGuard(this.getVOCDensity.bind(this)) + ); + + this.humiditySensorService = + this.accessory.getService(this.platform.Service.HumiditySensor) || + this.accessory.addService(this.platform.Service.HumiditySensor); + + this.humiditySensorService + .getCharacteristic( + this.platform.Characteristic.CurrentRelativeHumidity + ) + .onGet( + this.getCharacteristicValueGuard( + this.getCurrentRelativeHumidity.bind(this) + ) + ); + + this.temperatureSensorService = + this.accessory.getService( + this.platform.Service.TemperatureSensor + ) || + this.accessory.addService(this.platform.Service.TemperatureSensor); + + this.temperatureSensorService + .getCharacteristic(this.platform.Characteristic.CurrentTemperature) + .onGet( + this.getCharacteristicValueGuard( + this.getCurrentTemperature.bind(this) + ) + ); + this.carbonDioxideSensorService = this.accessory.getService( this.platform.Service.CarbonDioxideSensor @@ -41,6 +120,58 @@ export class PureA9 extends AirPurifier { ); } + async getIonizer(): Promise { + return this.appliance.properties.reported.Ionizer; + } + + async setIonizer(value: CharacteristicValue) { + await this.sendCommand({ + Ionizer: value + }); + + this.appliance.properties.reported.Ionizer = value as boolean; + } + + async getAirQuality(): Promise { + if (this.appliance.properties.reported.PM2_5 <= 25) { + return this.platform.Characteristic.AirQuality.EXCELLENT; + } else if (this.appliance.properties.reported.PM2_5 <= 50) { + return this.platform.Characteristic.AirQuality.GOOD; + } else if (this.appliance.properties.reported.PM2_5 <= 75) { + return this.platform.Characteristic.AirQuality.FAIR; + } else if (this.appliance.properties.reported.PM2_5 <= 100) { + return this.platform.Characteristic.AirQuality.INFERIOR; + } else { + return this.platform.Characteristic.AirQuality.POOR; + } + } + + async getPM2_5Density(): Promise { + return this.appliance.properties.reported.PM2_5; + } + + async getPM10Density(): Promise { + return this.appliance.properties.reported.PM10; + } + + async getVOCDensity(): Promise { + const vocDensity = tvocPPBToVocDensity( + this.appliance.properties.reported.TVOC, + this.appliance.properties.reported.Temp, + this._platform.config.vocMolecularWeight ?? 30.026 + ); + + return vocDensity; + } + + async getCurrentRelativeHumidity(): Promise { + return this.appliance.properties.reported.Humidity; + } + + async getCurrentTemperature(): Promise { + return this.appliance.properties.reported.Temp; + } + async getCarbonDioxideDetected(): Promise { return this.appliance.properties.reported.ECO2 > this.platform.config.carbonDioxideSensorAlarmValue @@ -57,6 +188,38 @@ export class PureA9 extends AirPurifier { async update(appliance: Appliance) { super.update(appliance); + this.ionizerService.updateCharacteristic( + this.platform.Characteristic.On, + this.appliance.properties.reported.Ionizer ? 1 : 0 + ); + + this.airQualityService.updateCharacteristic( + this.platform.Characteristic.AirQuality, + await this.getAirQuality() + ); + this.airQualityService.updateCharacteristic( + this.platform.Characteristic.PM2_5Density, + this.appliance.properties.reported.PM2_5 + ); + this.airQualityService.updateCharacteristic( + this.platform.Characteristic.PM10Density, + this.appliance.properties.reported.PM10 + ); + this.airQualityService.updateCharacteristic( + this.platform.Characteristic.VOCDensity, + this.appliance.properties.reported.TVOC + ); + + this.humiditySensorService.updateCharacteristic( + this.platform.Characteristic.CurrentRelativeHumidity, + this.appliance.properties.reported.Humidity + ); + + this.temperatureSensorService.updateCharacteristic( + this.platform.Characteristic.CurrentTemperature, + this.appliance.properties.reported.Temp + ); + this.carbonDioxideSensorService.updateCharacteristic( this.platform.Characteristic.CarbonDioxideDetected, this.appliance.properties.reported.CO2 > diff --git a/src/accessories/devices/airPurifier/ultimateHome500.ts b/src/accessories/devices/airPurifier/ultimateHome500.ts new file mode 100644 index 0000000..6054b1a --- /dev/null +++ b/src/accessories/devices/airPurifier/ultimateHome500.ts @@ -0,0 +1,88 @@ +import { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; +import { ElectroluxDevicesPlatform } from '../../../platform'; +import { AirPurifier } from './airPurifier'; +import { ElectroluxAccessoryController } from '../../controller'; +import { Appliance } from '../../../definitions/appliance'; + +export class UltimateHome500 extends AirPurifier { + private uvLightService: Service; + private airQualityService: Service; + + constructor( + readonly _platform: ElectroluxDevicesPlatform, + readonly _accessory: PlatformAccessory, + readonly _appliance: Appliance + ) { + super(_platform, _accessory, _appliance); + + this.uvLightService = + this.accessory.getService(this.platform.Service.Lightbulb) || + this.accessory.addService(this.platform.Service.Lightbulb); + + this.uvLightService.setCharacteristic( + this.platform.Characteristic.Name, + 'UV Light' + ); + + this.uvLightService + .getCharacteristic(this.platform.Characteristic.On) + .onGet(this.getCharacteristicValueGuard(this.getUVLight.bind(this))) + .onSet( + this.setCharacteristicValueGuard(this.setUVLight.bind(this)) + ); + + this.airQualityService = + this.accessory.getService(this.platform.Service.AirQualitySensor) || + this.accessory.addService(this.platform.Service.AirQualitySensor); + + this.airQualityService + .getCharacteristic(this.platform.Characteristic.AirQuality) + .onGet( + this.getCharacteristicValueGuard(this.getAirQuality.bind(this)) + ); + + this.airQualityService + .getCharacteristic(this.platform.Characteristic.PM2_5Density) + .onGet( + this.getCharacteristicValueGuard( + this.getPM2_5Density.bind(this) + ) + ); + } + + async getUVLight(): Promise { + return this.appliance.properties.reported.UVState === 'on'; + } + + async setUVLight(value: CharacteristicValue) { + await this.sendCommand({ + UVState: value ? 'On' : 'Off' + }); + + this.appliance.properties.reported.UVState = value ? 'on' : 'off'; + } + + async getAirQuality(): Promise { + if (this.appliance.properties.reported.PM2_5_approximate <= 25) { + return this.platform.Characteristic.AirQuality.EXCELLENT; + } else if (this.appliance.properties.reported.PM2_5_approximate <= 50) { + return this.platform.Characteristic.AirQuality.GOOD; + } else if (this.appliance.properties.reported.PM2_5_approximate <= 75) { + return this.platform.Characteristic.AirQuality.FAIR; + } else if ( + this.appliance.properties.reported.PM2_5_approximate <= 100 + ) { + return this.platform.Characteristic.AirQuality.INFERIOR; + } else { + return this.platform.Characteristic.AirQuality.POOR; + } + } + + async getPM2_5Density(): Promise { + return this.appliance.properties.reported.PM2_5_approximate; + } + + async update(appliance: Appliance) { + super.update(appliance); + } +} diff --git a/src/accessories/devices/airPurifier/wellA7.ts b/src/accessories/devices/airPurifier/wellA7.ts index a2cb0e7..30773ba 100644 --- a/src/accessories/devices/airPurifier/wellA7.ts +++ b/src/accessories/devices/airPurifier/wellA7.ts @@ -3,8 +3,13 @@ import { ElectroluxDevicesPlatform } from '../../../platform'; import { AirPurifier } from './airPurifier'; import { ElectroluxAccessoryController } from '../../controller'; import { Appliance } from '../../../definitions/appliance'; +import { tvocPPBToVocDensity } from '../../../util/voc'; export class WellA7 extends AirPurifier { + private ionizerService: Service; + private airQualityService: Service; + private humiditySensorService: Service; + private temperatureSensorService: Service; private carbonDioxideSensorService: Service; constructor( @@ -14,6 +19,80 @@ export class WellA7 extends AirPurifier { ) { super(_platform, _accessory, _appliance); + this.ionizerService = + this.accessory.getService(this.platform.Service.Switch) || + this.accessory.addService(this.platform.Service.Switch); + + this.ionizerService.setCharacteristic( + this.platform.Characteristic.Name, + 'Ionizer' + ); + + this.ionizerService + .getCharacteristic(this.platform.Characteristic.On) + .onGet(this.getCharacteristicValueGuard(this.getIonizer.bind(this))) + .onSet( + this.setCharacteristicValueGuard(this.setIonizer.bind(this)) + ); + + this.airQualityService = + this.accessory.getService(this.platform.Service.AirQualitySensor) || + this.accessory.addService(this.platform.Service.AirQualitySensor); + + this.airQualityService + .getCharacteristic(this.platform.Characteristic.AirQuality) + .onGet( + this.getCharacteristicValueGuard(this.getAirQuality.bind(this)) + ); + + this.airQualityService + .getCharacteristic(this.platform.Characteristic.PM2_5Density) + .onGet( + this.getCharacteristicValueGuard( + this.getPM2_5Density.bind(this) + ) + ); + + this.airQualityService + .getCharacteristic(this.platform.Characteristic.PM10Density) + .onGet( + this.getCharacteristicValueGuard(this.getPM10Density.bind(this)) + ); + + this.airQualityService + .getCharacteristic(this.platform.Characteristic.VOCDensity) + .onGet( + this.getCharacteristicValueGuard(this.getVOCDensity.bind(this)) + ); + + this.humiditySensorService = + this.accessory.getService(this.platform.Service.HumiditySensor) || + this.accessory.addService(this.platform.Service.HumiditySensor); + + this.humiditySensorService + .getCharacteristic( + this.platform.Characteristic.CurrentRelativeHumidity + ) + .onGet( + this.getCharacteristicValueGuard( + this.getCurrentRelativeHumidity.bind(this) + ) + ); + + this.temperatureSensorService = + this.accessory.getService( + this.platform.Service.TemperatureSensor + ) || + this.accessory.addService(this.platform.Service.TemperatureSensor); + + this.temperatureSensorService + .getCharacteristic(this.platform.Characteristic.CurrentTemperature) + .onGet( + this.getCharacteristicValueGuard( + this.getCurrentTemperature.bind(this) + ) + ); + this.carbonDioxideSensorService = this.accessory.getService( this.platform.Service.CarbonDioxideSensor @@ -41,6 +120,58 @@ export class WellA7 extends AirPurifier { ); } + async getIonizer(): Promise { + return this.appliance.properties.reported.Ionizer; + } + + async setIonizer(value: CharacteristicValue) { + await this.sendCommand({ + Ionizer: value + }); + + this.appliance.properties.reported.Ionizer = value as boolean; + } + + async getAirQuality(): Promise { + if (this.appliance.properties.reported.PM2_5 <= 25) { + return this.platform.Characteristic.AirQuality.EXCELLENT; + } else if (this.appliance.properties.reported.PM2_5 <= 50) { + return this.platform.Characteristic.AirQuality.GOOD; + } else if (this.appliance.properties.reported.PM2_5 <= 75) { + return this.platform.Characteristic.AirQuality.FAIR; + } else if (this.appliance.properties.reported.PM2_5 <= 100) { + return this.platform.Characteristic.AirQuality.INFERIOR; + } else { + return this.platform.Characteristic.AirQuality.POOR; + } + } + + async getPM2_5Density(): Promise { + return this.appliance.properties.reported.PM2_5; + } + + async getPM10Density(): Promise { + return this.appliance.properties.reported.PM10; + } + + async getVOCDensity(): Promise { + const vocDensity = tvocPPBToVocDensity( + this.appliance.properties.reported.TVOC, + this.appliance.properties.reported.Temp, + this._platform.config.vocMolecularWeight ?? 30.026 + ); + + return vocDensity; + } + + async getCurrentRelativeHumidity(): Promise { + return this.appliance.properties.reported.Humidity; + } + + async getCurrentTemperature(): Promise { + return this.appliance.properties.reported.Temp; + } + async getCarbonDioxideDetected(): Promise { return this.appliance.properties.reported.ECO2 > this.platform.config.carbonDioxideSensorAlarmValue @@ -57,6 +188,38 @@ export class WellA7 extends AirPurifier { async update(appliance: Appliance) { super.update(appliance); + this.ionizerService.updateCharacteristic( + this.platform.Characteristic.On, + this.appliance.properties.reported.Ionizer ? 1 : 0 + ); + + this.airQualityService.updateCharacteristic( + this.platform.Characteristic.AirQuality, + await this.getAirQuality() + ); + this.airQualityService.updateCharacteristic( + this.platform.Characteristic.PM2_5Density, + this.appliance.properties.reported.PM2_5 + ); + this.airQualityService.updateCharacteristic( + this.platform.Characteristic.PM10Density, + this.appliance.properties.reported.PM10 + ); + this.airQualityService.updateCharacteristic( + this.platform.Characteristic.VOCDensity, + this.appliance.properties.reported.TVOC + ); + + this.humiditySensorService.updateCharacteristic( + this.platform.Characteristic.CurrentRelativeHumidity, + this.appliance.properties.reported.Humidity + ); + + this.temperatureSensorService.updateCharacteristic( + this.platform.Characteristic.CurrentTemperature, + this.appliance.properties.reported.Temp + ); + this.carbonDioxideSensorService.updateCharacteristic( this.platform.Characteristic.CarbonDioxideDetected, this.appliance.properties.reported.ECO2 > diff --git a/src/const/devices.ts b/src/const/devices.ts index 65d6e5e..369af5b 100644 --- a/src/const/devices.ts +++ b/src/const/devices.ts @@ -1,6 +1,7 @@ import { Comfort600 } from '../accessories/devices/comfort600'; import { WellA7 } from '../accessories/devices/airPurifier/wellA7'; import { PureA9 } from '../accessories/devices/airPurifier/pureA9'; +import { UltimateHome500 } from '../accessories/devices/airPurifier/ultimateHome500'; export const DEVICES = { /* Air conditioners */ @@ -8,5 +9,6 @@ export const DEVICES = { /* Air purifiers */ WELLA7: WellA7, - PUREA9: PureA9 + PUREA9: PureA9, + Muju: UltimateHome500 }; diff --git a/src/definitions/appliance.ts b/src/definitions/appliance.ts index 34acc7d..e82bd2b 100644 --- a/src/definitions/appliance.ts +++ b/src/definitions/appliance.ts @@ -29,6 +29,12 @@ type ApplianceProperties = { /* Air purifiers */ Workmode: WorkMode; Fanspeed: number; + FilterLife_1: number; + FilterType_1: FilterType; + FilterLife_2: number; + FilterType_2: FilterType; + + /* Well A7, Pure A9 */ Ionizer: boolean; UILight: boolean; SafetyLock: boolean; @@ -38,16 +44,16 @@ type ApplianceProperties = { Temp: number; Humidity: number; TVOC: number; - FilterLife_1: number; - FilterType_1: FilterType; - FilterLife_2: number; - FilterType_2: FilterType; /* Well A7 */ ECO2: number; /* Pure A9 */ CO2: number; + + /* Extreme Home 500 */ + UVState: Toggle; + PM2_5_approximate: number; }; /* Comfort 600 */ diff --git a/tsconfig.json b/tsconfig.json index 1f2b606..2c0614a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,27 +1,17 @@ { - "compilerOptions": { - "target": "ES2018", // ~node10 - "module": "commonjs", - "lib": [ - "es2015", - "es2016", - "es2017", - "es2018", - "es2021" - ], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "noImplicitAny": false - }, - "include": [ - "src/" - ], - "exclude": [ - "**/*.spec.ts" - ] + "compilerOptions": { + "target": "ES2018", // ~node10 + "module": "commonjs", + "lib": ["es2015", "es2016", "es2017", "es2018", "es2021"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "noImplicitAny": false + }, + "include": ["src/"], + "exclude": ["**/*.spec.ts"] } From c89a15069eb698b9297387b0f154c2567a530748 Mon Sep 17 00:00:00 2001 From: tomekkleszcz Date: Sun, 10 Mar 2024 18:30:17 +0100 Subject: [PATCH 04/11] chore: 0.1.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c463785..ed91320 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "displayName": "Homebridge Electrolux Devices", "name": "homebridge-electrolux-devices", - "version": "0.0.10", + "version": "0.1.0", "description": "Homebridge plugin for Electrolux devices", "license": "Apache-2.0", "repository": { From 91153061defcbfefbc510fd0421bf49e5c8c2440 Mon Sep 17 00:00:00 2001 From: Tomek Kleszcz Date: Sun, 10 Mar 2024 18:30:53 +0100 Subject: [PATCH 05/11] fix: provide email when getting identity providers (#49) --- src/platform.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/platform.ts b/src/platform.ts index 74f36c1..b2abe75 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -119,7 +119,7 @@ export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin { ); const regionResponse = await axiosApi.get( - '/one-account-user/api/v1/identity-providers', + `/one-account-user/api/v1/identity-providers?email=${encodeURIComponent(this.config.email)}&loginType=OTP`, { headers: { Authorization: `Bearer ${tokenResponse.data.accessToken}` @@ -248,7 +248,6 @@ export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin { async refreshAccessToken() { if (!this.refreshToken) { - await this.signIn(); return; } @@ -290,6 +289,10 @@ export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin { must not be registered again to prevent "duplicate UUID" errors. */ async discoverDevices() { + if (!this.accessToken) { + return; + } + this.log.info('Discovering devices...'); const appliances = await this.getAppliances(); From b44005e70332661c8363820b361821ba1449ad95 Mon Sep 17 00:00:00 2001 From: tomekkleszcz Date: Sun, 10 Mar 2024 18:31:50 +0100 Subject: [PATCH 06/11] chore: 0.1.1 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ed91320..61178ee 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "displayName": "Homebridge Electrolux Devices", "name": "homebridge-electrolux-devices", - "version": "0.1.0", + "version": "0.1.1", "description": "Homebridge plugin for Electrolux devices", "license": "Apache-2.0", "repository": { From bbdd9ab2c9efc3f1995f2fe9ec2e4d2236cedc2f Mon Sep 17 00:00:00 2001 From: Tomek Kleszcz Date: Mon, 11 Mar 2024 17:12:39 +0100 Subject: [PATCH 07/11] fix(air purifier): fix voc density exceeded maximum of 1000 warning (#51) --- src/accessories/devices/airPurifier/pureA9.ts | 9 +++++++-- src/accessories/devices/airPurifier/wellA7.ts | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/accessories/devices/airPurifier/pureA9.ts b/src/accessories/devices/airPurifier/pureA9.ts index a2fd0e5..8fcb92a 100644 --- a/src/accessories/devices/airPurifier/pureA9.ts +++ b/src/accessories/devices/airPurifier/pureA9.ts @@ -161,7 +161,12 @@ export class PureA9 extends AirPurifier { this._platform.config.vocMolecularWeight ?? 30.026 ); - return vocDensity; + return Math.min( + vocDensity, + this.airQualityService.getCharacteristic( + this.platform.Characteristic.VOCDensity + ).props.maxValue! + ); } async getCurrentRelativeHumidity(): Promise { @@ -207,7 +212,7 @@ export class PureA9 extends AirPurifier { ); this.airQualityService.updateCharacteristic( this.platform.Characteristic.VOCDensity, - this.appliance.properties.reported.TVOC + await this.getVOCDensity() ); this.humiditySensorService.updateCharacteristic( diff --git a/src/accessories/devices/airPurifier/wellA7.ts b/src/accessories/devices/airPurifier/wellA7.ts index 30773ba..b1f4c34 100644 --- a/src/accessories/devices/airPurifier/wellA7.ts +++ b/src/accessories/devices/airPurifier/wellA7.ts @@ -161,7 +161,12 @@ export class WellA7 extends AirPurifier { this._platform.config.vocMolecularWeight ?? 30.026 ); - return vocDensity; + return Math.min( + vocDensity, + this.airQualityService.getCharacteristic( + this.platform.Characteristic.VOCDensity + ).props.maxValue! + ); } async getCurrentRelativeHumidity(): Promise { @@ -207,7 +212,7 @@ export class WellA7 extends AirPurifier { ); this.airQualityService.updateCharacteristic( this.platform.Characteristic.VOCDensity, - this.appliance.properties.reported.TVOC + await this.getVOCDensity() ); this.humiditySensorService.updateCharacteristic( From c7274abbcaed013fab2e90edcd4f8720a63a8806 Mon Sep 17 00:00:00 2001 From: Tomek Kleszcz Date: Mon, 11 Mar 2024 17:30:44 +0100 Subject: [PATCH 08/11] docs: add badges (#52) --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d7df309..966869e 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ # Homebridge Electrolux Devices +![NPM Downloads](https://img.shields.io/npm/dm/homebridge-electrolux-devices) +![NPM Version](https://img.shields.io/npm/v/homebridge-electrolux-devices) +![Tests](https://github.com/tomekkleszcz/homebridge-electrolux-devices/actions/workflows/tests.yml/badge.svg) + This is a plugin for connecting Electrolux devices which are controlled by the [Electrolux](https://apps.apple.com/pl/app/electrolux/id1595816832) app to Homekit. ## 🧰 Installation @@ -19,6 +23,6 @@ This is a plugin for connecting Electrolux devices which are controlled by the [ - Comfort 600 air conditioner - Well A7 air purifier - Pure A9/AX 9 air purifier -- Electrolux UltimateHome 500 +- UltimateHome 500 If your device is not on the list, please create the issue. I'll be more than happy to implement the support for your device. 😄 From 585167e3dbd0c607e717b4af8dad38b4504dfc91 Mon Sep 17 00:00:00 2001 From: tomekkleszcz Date: Mon, 11 Mar 2024 17:31:19 +0100 Subject: [PATCH 09/11] chore: 0.1.2 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 61178ee..cb84595 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "displayName": "Homebridge Electrolux Devices", "name": "homebridge-electrolux-devices", - "version": "0.1.1", + "version": "0.1.2", "description": "Homebridge plugin for Electrolux devices", "license": "Apache-2.0", "repository": { From 91a6ccc3ba1af6dfe3d4fe9a24f8c9ab90c46f5a Mon Sep 17 00:00:00 2001 From: tomekkleszcz Date: Mon, 11 Mar 2024 17:35:18 +0100 Subject: [PATCH 10/11] docs: change github actions workflow status badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 966869e..daa5f0d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ![NPM Downloads](https://img.shields.io/npm/dm/homebridge-electrolux-devices) ![NPM Version](https://img.shields.io/npm/v/homebridge-electrolux-devices) -![Tests](https://github.com/tomekkleszcz/homebridge-electrolux-devices/actions/workflows/tests.yml/badge.svg) +![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/tomekkleszcz/homebridge-electrolux-devices/tests.yml?label=tests) This is a plugin for connecting Electrolux devices which are controlled by the [Electrolux](https://apps.apple.com/pl/app/electrolux/id1595816832) app to Homekit. From a15cdc7bb6ce139e47e2b6b2729f4ba2d1153fb4 Mon Sep 17 00:00:00 2001 From: tomekkleszcz Date: Mon, 11 Mar 2024 17:40:32 +0100 Subject: [PATCH 11/11] docs: add air purifier next to ultimate home 500 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index daa5f0d..e939012 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,6 @@ This is a plugin for connecting Electrolux devices which are controlled by the [ - Comfort 600 air conditioner - Well A7 air purifier - Pure A9/AX 9 air purifier -- UltimateHome 500 +- UltimateHome 500 air purifier If your device is not on the list, please create the issue. I'll be more than happy to implement the support for your device. 😄