From 764a3ca2236457d3fa675fb11a31570100f26385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 29 Mar 2021 12:48:27 +0200 Subject: [PATCH 01/40] Add typings and jsdoc comments to improve developer experience --- plejd/Configuration.js | 8 +- plejd/PlejdBLEHandler.js | 4 +- plejd/README.md | 5 + plejd/jsconfig.json | 10 + plejd/types/ApiSite.d.ts | 655 ++++++++++++++++++++++++++++++++ plejd/types/Configuration.d.ts | 50 +++ plejd/types/DeviceRegistry.d.ts | 21 + plejd/types/PlejdApi.d.ts | 11 + plejd/typings.json | 5 + 9 files changed, 765 insertions(+), 4 deletions(-) create mode 100644 plejd/jsconfig.json create mode 100644 plejd/types/ApiSite.d.ts create mode 100644 plejd/types/Configuration.d.ts create mode 100644 plejd/types/DeviceRegistry.d.ts create mode 100644 plejd/types/PlejdApi.d.ts create mode 100644 plejd/typings.json diff --git a/plejd/Configuration.js b/plejd/Configuration.js index 88a02b3..4b62689 100644 --- a/plejd/Configuration.js +++ b/plejd/Configuration.js @@ -1,9 +1,12 @@ const fs = require('fs'); class Configuration { + /** @type {import('types/Configuration').Options} */ static _options = null; + /** @type {import('types/Configuration').AddonInfo} */ static _addonInfo = null; + /** @returns Options */ static getOptions() { if (!Configuration._options) { Configuration._hydrateCache(); @@ -11,6 +14,7 @@ class Configuration { return Configuration._options; } + /** @returns AddonInfo */ static getAddonInfo() { if (!Configuration._addonInfo) { Configuration._hydrateCache(); @@ -20,10 +24,10 @@ class Configuration { static _hydrateCache() { const rawData = fs.readFileSync('/data/options.json'); - const config = JSON.parse(rawData); + const config = JSON.parse(rawData.toString()); const defaultRawData = fs.readFileSync('/plejd/config.json'); - const defaultConfig = JSON.parse(defaultRawData); + const defaultConfig = JSON.parse(defaultRawData.toString()); Configuration._options = { ...defaultConfig.options, ...config }; Configuration._addonInfo = { diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 9256324..84f1329 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -1,7 +1,7 @@ const dbus = require('dbus-next'); const crypto = require('crypto'); const xor = require('buffer-xor'); -const EventEmitter = require('events'); +const { EventEmitter } = require('events'); const Configuration = require('./Configuration'); const constants = require('./constants'); @@ -874,7 +874,7 @@ class PlejBLEHandler extends EventEmitter { (pl) => pl.writeInt32LE(Math.trunc(newLocalTimestamp), 5), ); try { - this.write(payload); + this._write(payload); } catch (err) { logger.error( 'Failed writing new time to Plejd. Will try again in one hour or at restart.', diff --git a/plejd/README.md b/plejd/README.md index dec014d..336ea57 100644 --- a/plejd/README.md +++ b/plejd/README.md @@ -189,6 +189,11 @@ The code in this project follows the [Airbnb JavaScript guide](https://github.co For a nice developer experience it is very convenient to have `eslint` and `prettier` installed in your favorite editor (such as VS Code) and use the "format on save" option (or invoke formatting by Alt+Shift+F in VS Code). Any code issues should appear in the problems window inside the editor, as well as when running the command above. +For partial type hinting you can run + +- `npm install --global typings` +- `typings install` + When contributing, please do so by forking the repo and then using pull requests towards the dev branch. ### Logs diff --git a/plejd/jsconfig.json b/plejd/jsconfig.json new file mode 100644 index 0000000..9599f8f --- /dev/null +++ b/plejd/jsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "checkJs": true, + "module": "commonjs", + "moduleResolution": "node", + "target": "es6" + }, + "exclude": ["node_modules", "rootfs"] +} diff --git a/plejd/types/ApiSite.d.ts b/plejd/types/ApiSite.d.ts new file mode 100644 index 0000000..856d056 --- /dev/null +++ b/plejd/types/ApiSite.d.ts @@ -0,0 +1,655 @@ +/* eslint-disable camelcase */ +/* eslint-disable no-use-before-define */ + +export interface CachedSite { + siteId: string; + siteDetails: ApiSite; + sessionToken: string; + dtCache: string; +} + +export interface ApiSite { + site: SiteDetailsSite; + plejdMesh: PlejdMesh; + rooms: Room[]; + scenes: Scene[]; + devices: Device[]; + plejdDevices: PlejdDevice[]; + gateways: Gateway[]; + resourceSets: ResourceSet[]; + timeEvents: TimeEvent[]; + sceneSteps: SceneStep[]; + astroEvents: AstroEvent[]; + inputSettings: InputSetting[]; + outputSettings: OutputSetting[]; + stateTimers: StateTimers; + sitePermission: SitePermission; + inputAddress: { [key: string]: { [key: string]: number } }; + outputAddress: { [key: string]: OutputAddress }; + deviceAddress: { [key: string]: number }; + outputGroups: { [key: string]: OutputGroup }; + roomAddress: { [key: string]: number }; + sceneIndex: { [key: string]: number }; + images: Images; + deviceLimit: number; +} + +export interface AstroEvent { + dirtyDevices?: any[]; + dirtyRemovedDevices?: any[]; + deviceId: string; + siteId: string; + sceneId: string; + fadeTime: number; + activated: boolean; + astroEventId: string; + index: number; + sunriseOffset: number; + sunsetOffset: number; + pauseStart: string; + pauseEnd: string; + createdAt: Date; + updatedAt: Date; + dirtyRemove?: boolean; + ACL: AstroEventACL; + targetDevices: AstroEventTargetDevice[]; + objectId: string; + __type: AstroEventType; + className: string; +} + +export interface AstroEventACL {} + +export enum AstroEventType { + Object = 'Object', +} + +export interface AstroEventTargetDevice { + deviceId: string; + index: number; +} + +export interface Device { + deviceId: string; + siteId: string; + roomId: string; + title: string; + traits: number; + hardware?: Hardware; + hiddenFromRoomList: boolean; + createdAt: Date; + updatedAt: Date; + outputType: OutputType; + ACL: AstroEventACL; + objectId: string; + __type: AstroEventType; + className: DeviceClassName; + hiddenFromIntegrations?: boolean; +} + +export enum DeviceClassName { + Device = 'Device', +} + +export interface Hardware { + createdAt: Date; + updatedAt: Date; + name: Name; + hardwareId: string; + minSupportedFirmware: PlejdMeshClass; + latestFirmware: PlejdMeshClass; + brand: Brand; + type: Type; + image: Image; + requiredAccountType: RequiredAccountType[]; + numberOfDevices: number; + predefinedLoad: PredefinedLoad; + supportedFirmware: PredefinedLoad; + ACL: AstroEventACL; + objectId: HardwareObjectID; + __type: AstroEventType; + className: HardwareClassName; +} + +export enum Brand { + PlejdLight = 'Plejd Light', +} + +export enum HardwareClassName { + Hardware = 'Hardware', +} + +export interface Image { + __type: ImageType; + name: string; + url: string; +} + +export enum ImageType { + File = 'File', +} + +export interface PlejdMeshClass { + __type: InstallerType; + className: SiteClassName; + objectId: ObjectID; +} + +export enum InstallerType { + Pointer = 'Pointer', +} + +export enum SiteClassName { + DimCurve = 'DimCurve', + Firmware = 'Firmware', + PlejdMesh = 'PlejdMesh', + Site = 'Site', + User = '_User', + UserProfile = 'UserProfile', +} + +export enum ObjectID { + BBBJO2Cufm = 'BBBJO2cufm', + D4Dw87Hq21 = 'D4DW87HQ21', + FCrrS1NJHH = 'FCrrS1nJHH', + GX1W4P06QS = 'gX1W4p06QS', + Ndlvzgh4Df = 'ndlvzgh4df', + UHoKQLuXqZ = 'uHoKQLuXqZ', + VfHiawBPA8 = 'vfHiawBPA8', + WgAFPloWjK = 'wgAfPloWjK', + YkyNDotBNa = 'YkyNDotBNa', +} + +export enum Name { + Ctr01 = 'CTR-01', + Dim01 = 'DIM-01', +} + +export enum HardwareObjectID { + R3Gfd6ACAu = 'R3gfd6ACAu', + XjslOltgvi = 'xjslOltgvi', +} + +export interface PredefinedLoad { + __type: SupportedFirmwareType; + className: PredefinedLoadClassName; +} + +export enum SupportedFirmwareType { + Relation = 'Relation', +} + +export enum PredefinedLoadClassName { + DimCurve = 'DimCurve', + Firmware = 'Firmware', + PredefinedLoad = 'PredefinedLoad', +} + +export enum RequiredAccountType { + Installer = 'installer', +} + +export enum Type { + Controller = 'Controller', + LEDDimmer = 'LED Dimmer', +} + +export enum OutputType { + Light = 'LIGHT', + Relay = 'RELAY', +} + +export interface Gateway { + title: string; + deviceId: string; + siteId: string; + hardwareId: string; + installer: ObjectID; + firmware: number; + firmwareObject: Firmware; + dirtyInstall: boolean; + dirtyUpdate: boolean; + createdAt: Date; + updatedAt: Date; + factoryKey: string; + resourceSetId: string; + ACL: AstroEventACL; + objectId: string; + __type: AstroEventType; + className: string; +} + +export interface Firmware { + notes: Notes; + createdAt: Date; + updatedAt: Date; + data: Image; + metaData: Image; + version: Version; + buildTime: number; + firmwareApi: string; + ACL: AstroEventACL; + objectId: FirmwareObjectObjectID; + __type: AstroEventType; + className: SiteClassName; +} + +export enum Notes { + Ctr01 = 'CTR-01', + Ctr20ReleaseCandidate1 = 'Ctr 2.0 Release candidate 1', + Dim20ReleaseCandidate1 = 'Dim 2.0 Release candidate 1', + Dim221ReleaseCandidate = 'Dim 2.2.1 Release Candidate', + GWY10ReleaseCandidate = 'GWY 1.0 Release Candidate', +} + +export enum FirmwareObjectObjectID { + BBBJO2Cufm = 'BBBJO2cufm', + E6YxfREDuF = 'E6yxfREDuF', + JYSZ0EvyCU = 'JYSZ0EvyCU', + Ndlvzgh4Df = 'ndlvzgh4df', + RlglTfVHDe = 'rlglTfVHDe', +} + +export enum Version { + The12 = '1.2', + The20 = '2.0', + The221 = '2.2.1', + The304 = '3.0.4', +} + +export interface Images { + '2afc6c6e-7a26-466a-b8ec-febbca90f5f7': string; +} + +export interface InputSetting { + deviceId: string; + input: number; + siteId: string; + dimSpeed: number; + buttonType: ButtonType; + createdAt: Date; + updatedAt: Date; + ACL: AstroEventACL; + objectId: string; + __type: AstroEventType; + className: InputSettingClassName; + doubleClick?: string; + singleClick?: null; + doubleSidedDirectionButton?: boolean; +} + +export enum ButtonType { + PushButton = 'PushButton', + RotateMesh = 'RotateMesh', + Scene = 'Scene', +} + +export enum InputSettingClassName { + PlejdDeviceInputSetting = 'PlejdDeviceInputSetting', +} + +export interface OutputAddress { + '0': number; +} + +export interface OutputGroup { + '0': number[]; +} + +export interface OutputSetting { + deviceId: string; + output: number; + deviceParseId: string; + siteId: string; + predefinedLoad: OutputSettingPredefinedLoad; + createdAt: Date; + updatedAt: Date; + dimMin: number; + dimMax: number; + dimStart: number; + outputStartTime: number; + outputSpeed: number; + bootState: BootState; + dimCurve: DimCurve; + curveLogarithm: number; + curveSinusCompensation: number; + curveRectification: boolean; + output_0_10V_Mode?: Output0_10_VMode; + zeroCrossing?: Output0_10_VMode; + minimumRelayOffTime?: number; + ACL: AstroEventACL; + objectId: string; + __type: AstroEventType; + className: OutputSettingClassName; + ledCurrent?: number; + ledVoltage?: number; + relayConfig?: Output0_10_VMode; +} + +export enum BootState { + UseLast = 'UseLast', +} + +export enum OutputSettingClassName { + PlejdDeviceOutputSetting = 'PlejdDeviceOutputSetting', +} + +export enum DimCurve { + LinearLogarithmicSlidingProportion = 'LinearLogarithmicSlidingProportion', + NonDimmable = 'NonDimmable', +} + +export enum Output0_10_VMode { + Unknown = 'Unknown', +} + +export interface OutputSettingPredefinedLoad { + updatedAt: Date; + createdAt: Date; + loadType: string; + predefinedLoadData: string; + defaultDimCurve: PlejdMeshClass; + description_en?: DescriptionEn; + title_en?: TitleEn; + title_sv?: TitleSv; + description_sv?: DescriptionSv; + titleKey: string; + descriptionKey: string; + allowedDimCurves: PredefinedLoad; + ACL: PredefinedLoadACL; + objectId: string; + __type: AstroEventType; + className: PredefinedLoadClassName; + supportMessage?: SupportMessage; + filters?: Filters; +} + +export interface PredefinedLoadACL { + '*': Empty; +} + +export interface Empty { + read: boolean; +} + +export enum DescriptionEn { + OnOff = 'On / Off', + OnlySwitchingOffOn = 'Only switching off/on', + The230VDimmableLEDLightSourceMax100VA = '230V dimmable LED light source - Max 100VA', + The230VIncandescentHalogenElectronicTransformatorMax300W = '230V Incandescent / Halogen, Electronic transformator - Max 300W', + WithoutRelay = 'Without relay', +} + +export enum DescriptionSv { + EndastBrytningAVPå = 'Endast brytning av/på', + ReläbrytningAVPå = 'Reläbrytning av/på', + The230VDimbarLEDLjuskällaMax100VA = '230V dimbar LED ljuskälla - Max 100VA', + The230VDimbarLEDLjuskällaMax200VA = '230V dimbar LED ljuskälla - Max 200VA', + The230VHalogenGlödljusElektroniskTransformatorMax300W = '230V Halogen / Glödljus, Elektronisk transformator - Max 300W', + UtanReläbrytning = 'Utan reläbrytning', +} + +export interface Filters { + allowedCountriesFilter: AllowedCountriesFilter; +} + +export interface AllowedCountriesFilter { + countryCodes: CountryCode[]; +} + +export enum CountryCode { + Fi = 'FI', + No = 'NO', + SE = 'SE', +} + +export enum SupportMessage { + PredefinedLoadNonDimmableSupportMessageHTML = 'PredefinedLoadNonDimmableSupportMessageHTML', +} + +export enum TitleEn { + IncandescentHalogen = 'Incandescent / Halogen', + LEDTrailingEdgeCommon = 'LED Trailing Edge (Common)', + LeadingEdge = 'Leading edge', + NonDimmableLEDLightSourceMax200VA = 'Non-dimmable LED light source (Max 200VA)', + RelayOnly = 'Relay only', + The010V = '0-10V', +} + +export enum TitleSv { + EjDimbarLEDLjuskällaMax200VA = 'Ej dimbar LED-ljuskälla (Max 200VA)', + HalogenGlödljus = 'Halogen / Glödljus', + LEDBakkantVanligast = 'LED Bakkant (Vanligast)', + LEDFramkant = 'LED Framkant', + Reläfunktion = 'Reläfunktion', + The010V = '0-10V', +} + +export interface PlejdDevice { + deviceId: string; + installer: PlejdMeshClass; + dirtyInstall: boolean; + dirtyUpdate: boolean; + dirtyClock: boolean; + hardwareId: string; + faceplateId: string; + firmware: Firmware; + createdAt: Date; + updatedAt: Date; + coordinates: Coordinates; + dirtySettings: boolean; + diagnostics: string; + siteId: string; + predefinedLoad: OutputSettingPredefinedLoad; + ACL: AstroEventACL; + objectId: string; + __type: AstroEventType; + className: PlejdDeviceClassName; +} + +export enum PlejdDeviceClassName { + PlejdDevice = 'PlejdDevice', +} + +export interface Coordinates { + __type: CoordinatesType; + latitude: number; + longitude: number; +} + +export enum CoordinatesType { + GeoPoint = 'GeoPoint', +} + +export interface PlejdMesh { + siteId: string; + plejdMeshId: string; + meshKey: string; + cryptoKey: string; + createdAt: Date; + updatedAt: Date; + site: PlejdMeshClass; + ACL: AstroEventACL; + objectId: ObjectID; + __type: AstroEventType; + className: SiteClassName; +} + +export interface ResourceSet { + scopes: string[]; + remoteAccessUsers: string[]; + name: string; + type: string; + createdAt: Date; + updatedAt: Date; + ACL: AstroEventACL; + objectId: string; + __type: AstroEventType; + className: string; +} + +export interface Room { + siteId: string; + roomId: string; + title: string; + category: string; + imageHash: number; + createdAt: Date; + updatedAt: Date; + ACL: AstroEventACL; + objectId: string; + __type: AstroEventType; + className: RoomClassName; +} + +export enum RoomClassName { + Room = 'Room', +} + +export interface SceneStep { + sceneId: string; + siteId: string; + deviceId: string; + state: State; + value: number; + output: number; + createdAt: Date; + updatedAt: Date; + ACL: AstroEventACL; + objectId: string; + __type: AstroEventType; + className: SceneStepClassName; + dirty?: boolean; + dirtyRemoved?: boolean; +} + +export enum SceneStepClassName { + SceneStep = 'SceneStep', +} + +export enum State { + Off = 'Off', + On = 'On', +} + +export interface Scene { + title: string; + sceneId: string; + siteId: string; + hiddenFromSceneList: boolean; + settings: string; + createdAt: Date; + updatedAt: Date; + ACL: AstroEventACL; + objectId: string; + __type: AstroEventType; + className: ButtonType; +} + +export interface SiteDetailsSite { + installers: ObjectID[]; + title: string; + siteId: string; + version: number; + createdAt: Date; + updatedAt: Date; + plejdMesh: PlejdMeshClass; + coordinates: Coordinates; + astroTable: AstroTable; + deviceAstroTable: DeviceAstroTable; + zipCode: string; + city: string; + country: string; + previousOwners: string[]; + ACL: AstroEventACL; + objectId: ObjectID; + __type: AstroEventType; + className: SiteClassName; +} + +export interface AstroTable { + sunrise: string[]; + sunset: string[]; +} + +export interface DeviceAstroTable { + sunrise: number[]; + sunset: number[]; +} + +export interface SitePermission { + siteId: string; + userId: ObjectID; + user: User; + isOwner: boolean; + isInstaller: boolean; + isUser: boolean; + site: SiteDetailsSite; + createdAt: Date; + updatedAt: Date; + ACL: AstroEventACL; + objectId: string; + __type: AstroEventType; + className: string; +} + +export interface User { + profileName: string; + isInstaller: boolean; + email: string; + locale: string; + username: string; + emailVerified: boolean; + createdAt: Date; + updatedAt: Date; + profile: PlejdMeshClass; + _failed_login_count: number; + hasIntegration: boolean; + ACL: UserACL; + objectId: ObjectID; + __type: AstroEventType; + className: SiteClassName; +} + +export interface UserACL { + gX1W4p06QS: GX1W4P06QS; +} + +export interface GX1W4P06QS { + read: boolean; + write: boolean; +} + +export interface StateTimers { + SafetyTimer: any[]; +} + +export interface TimeEvent { + dirtyDevices?: any[]; + dirtyRemovedDevices?: any[]; + scheduledDays: number[]; + deviceId: string; + siteId: string; + sceneId: string; + fadeTime: number; + activated: boolean; + timeEventId: string; + startTimeIndex: number; + endTimeIndex: number; + startTime: string; + endTime: string; + createdAt: Date; + updatedAt: Date; + dirtyRemove?: boolean; + ACL: AstroEventACL; + targetDevices: TimeEventTargetDevice[]; + objectId: string; + __type: AstroEventType; + className: string; +} + +export interface TimeEventTargetDevice { + deviceId: string; + startTimeIndex: number; + endTimeIndex: number; +} diff --git a/plejd/types/Configuration.d.ts b/plejd/types/Configuration.d.ts new file mode 100644 index 0000000..016d19b --- /dev/null +++ b/plejd/types/Configuration.d.ts @@ -0,0 +1,50 @@ +/* eslint-disable no-use-before-define */ + +export interface AddonInfo { + name: string; + version: string; + slug: string; + description: string; + url: string; + arch: string[]; + startup: string; + boot: string; + host_network: boolean; + host_dbus: boolean; + apparmor: boolean; +} + +export interface Configuration extends AddonInfo { + options: Options; + schema: Schema; +} + +export interface Options { + site: string; + username: string; + password: string; + mqttBroker: string; + mqttUsername: string; + mqttPassword: string; + includeRoomsAsLights: boolean; + preferCachedApiResponse: boolean; + updatePlejdClock: boolean; + logLevel: string; + connectionTimeout: number; + writeQueueWaitTime: number; +} + +export interface Schema { + site: string; + username: string; + password: string; + mqttBroker: string; + mqttUsername: string; + mqttPassword: string; + includeRoomsAsLights: string; + preferCachedApiResponse: string; + updatePlejdClock: string; + logLevel: string; + connectionTimeout: string; + writeQueueWaitTime: string; +} diff --git a/plejd/types/DeviceRegistry.d.ts b/plejd/types/DeviceRegistry.d.ts new file mode 100644 index 0000000..4dc373f --- /dev/null +++ b/plejd/types/DeviceRegistry.d.ts @@ -0,0 +1,21 @@ +/* eslint-disable no-use-before-define */ + +export type OutputDevices = { [deviceIdAndOutput: string]: OutputDevice }; + +export interface OutputDevice { + bleDeviceIndex: number; + deviceId: string; + dim?: number; + dimmable: boolean; + hiddenFromRoomList?: boolean; + hiddenFromIntegrations?: boolean; + hiddenFromSceneList?: boolean; + name: string; + output: number; + roomId: string; + state: number | undefined; + type: string; + typeName: string; + version: string; + uniqueId: string; +} diff --git a/plejd/types/PlejdApi.d.ts b/plejd/types/PlejdApi.d.ts new file mode 100644 index 0000000..1cbdbe8 --- /dev/null +++ b/plejd/types/PlejdApi.d.ts @@ -0,0 +1,11 @@ +/* eslint-disable no-use-before-define */ + +import { ApiSite } from './ApiSite'; + +export type PlejdApi = { + config: any; + deviceRegistry: any; + sessionToken: string; + siteId: string; + siteDetails: ApiSite; +}; diff --git a/plejd/typings.json b/plejd/typings.json new file mode 100644 index 0000000..0fd4334 --- /dev/null +++ b/plejd/typings.json @@ -0,0 +1,5 @@ +{ + "globalDependencies": { + "node": "registry:dt/node#7.0.0+20170322231424" + } +} From d6b9d9104a8ef19d4313be1d3b4ed7fbf12061f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 29 Mar 2021 12:51:48 +0200 Subject: [PATCH 02/40] Refactor DeviceRegistry to start use unique ids based on device serial and output index --- plejd/DeviceRegistry.js | 152 +++++++++------------- plejd/PlejdApi.js | 205 +++++++++++++++++++++--------- plejd/PlejdDeviceCommunication.js | 21 +-- 3 files changed, 214 insertions(+), 164 deletions(-) diff --git a/plejd/DeviceRegistry.js b/plejd/DeviceRegistry.js index 5912ba8..e1fb753 100644 --- a/plejd/DeviceRegistry.js +++ b/plejd/DeviceRegistry.js @@ -2,125 +2,94 @@ const Logger = require('./Logger'); const logger = Logger.getLogger('device-registry'); class DeviceRegistry { - apiSite; + /** @type {string} */ cryptoKey = null; - deviceIdsByRoom = {}; - deviceIdsBySerial = {}; + outputDeviceIdByRoomId = {}; + outputDeviceIdByBLEIndex = {}; // Dictionaries of [id]: device per type - plejdDevices = {}; - roomDevices = {}; + /** @type {import('types/DeviceRegistry').OutputDevices} */ + outputDevices = {}; + /** @type {import('types/DeviceRegistry').OutputDevices} */ sceneDevices = {}; - get allDevices() { - return [ - ...Object.values(this.plejdDevices), - ...Object.values(this.roomDevices), - ...Object.values(this.sceneDevices), - ]; + // eslint-disable-next-line class-methods-use-this + getUniqueOutputId(deviceId, outputIndex) { + return `${deviceId}_${outputIndex}`; } - addPlejdDevice(device) { - const added = { - ...this.plejdDevices[device.id], - ...device, + /** @param outputDevice {import('types/DeviceRegistry').OutputDevice} */ + addOutputDevice(outputDevice) { + this.outputDevices = { + ...this.outputDevices, + [outputDevice.uniqueId]: outputDevice, }; - this.plejdDevices = { - ...this.plejdDevices, - [added.id]: added, - }; - - this.deviceIdsBySerial[added.serialNumber] = added.id; - logger.verbose( - `Added/updated device: ${JSON.stringify(added)}. ${ - Object.keys(this.plejdDevices).length - } plejd devices in total.`, + `Added/updated output device: ${JSON.stringify(outputDevice)}. ${ + Object.keys(this.outputDevices).length + } output devices in total.`, ); - if (added.roomId) { - if (!this.deviceIdsByRoom[added.roomId]) { - this.deviceIdsByRoom[added.roomId] = []; - } - const room = this.deviceIdsByRoom[added.roomId]; - if (!room.includes(added.id)) { - this.deviceIdsByRoom[added.roomId] = [...room, added.id]; - } + this.outputDeviceIdByBLEIndex[outputDevice.bleDeviceIndex] = outputDevice.uniqueId; + + if (!this.outputDeviceIdByRoomId[outputDevice.roomId]) { + this.outputDeviceIdByRoomId[outputDevice.roomId] = []; + } + if ( + outputDevice.roomId !== outputDevice.uniqueId + && !this.outputDeviceIdByRoomId[outputDevice.roomId].includes(outputDevice.roomId) + ) { + this.outputDeviceIdByRoomId[outputDevice.roomId].push(outputDevice.roomId); logger.verbose( - `Added device to room ${added.roomId}: ${JSON.stringify( - this.deviceIdsByRoom[added.roomId], + `Added device to room ${outputDevice.roomId}: ${JSON.stringify( + this.outputDeviceIdByRoomId[outputDevice.roomId], )}`, ); } - return added; - } - - addRoomDevice(device) { - const added = { - ...this.roomDevices[device.id], - ...device, - }; - this.roomDevices = { - ...this.roomDevices, - [added.id]: added, - }; - - logger.verbose( - `Added/updated room device: ${JSON.stringify(added)}. ${ - Object.keys(this.roomDevices).length - } room devices total.`, - ); - return added; + if (outputDevice.hiddenFromIntegrations || outputDevice.hiddenFromRoomList) { + logger.verbose(`Device is hidden and should possibly not be included. + Hidden from room list: ${outputDevice.hiddenFromRoomList} + Hidden from integrations: ${outputDevice.hiddenFromIntegrations}`); + } } + /** @param scene {import('types/DeviceRegistry').OutputDevice} */ addScene(scene) { - const added = { - ...this.sceneDevices[scene.id], - ...scene, - }; this.sceneDevices = { ...this.sceneDevices, - [added.id]: added, + [scene.uniqueId]: scene, }; logger.verbose( - `Added/updated scene: ${JSON.stringify(added)}. ${ + `Added/updated scene: ${JSON.stringify(scene)}. ${ Object.keys(this.sceneDevices).length } scenes in total.`, ); - return added; } clearPlejdDevices() { - this.plejdDevices = {}; - this.deviceIdsByRoom = {}; + this.outputDevices = {}; + this.outputDeviceIdByRoomId = {}; this.deviceIdsBySerial = {}; } - clearRoomDevices() { - this.roomDevices = {}; - } - clearSceneDevices() { this.sceneDevices = {}; } - getDevice(deviceId) { - return this.plejdDevices[deviceId] || this.roomDevices[deviceId]; + getOutputDevice(uniqueOutputId) { + return this.outputDevices[uniqueOutputId]; } - getDeviceIdsByRoom(roomId) { - return this.deviceIdsByRoom[roomId]; + /** @returns {string[]} */ + getOutputDeviceIdsByRoomId(roomId) { + return this.outputDeviceIdByRoomId[roomId]; } - getDeviceBySerialNumber(serialNumber) { - return this.getDevice(this.deviceIdsBySerial[serialNumber]); - } - - getDeviceName(deviceId) { - return (this.plejdDevices[deviceId] || {}).name; + getOutputDeviceName(uniqueOutputId) { + return (this.outputDevices[uniqueOutputId] || {}).name; } getScene(sceneId) { @@ -131,25 +100,20 @@ class DeviceRegistry { return (this.sceneDevices[sceneId] || {}).name; } - getState(deviceId) { - const device = this.getDevice(deviceId) || {}; - if (device.dimmable) { - return { - state: device.state, - dim: device.dim, - }; + /** + * @param {string} uniqueOutputId + * @param {boolean} state + * @param {number?} [dim] + */ + setOutputState(uniqueOutputId, state, dim) { + const device = this.getOutputDevice(uniqueOutputId); + if (!device) { + logger.warn( + `Trying to set state for ${uniqueOutputId} which is not in the list of known outputs.`, + ); + return; } - return { - state: device.state, - }; - } - setApiSite(siteDetails) { - this.apiSite = siteDetails; - } - - setState(deviceId, state, dim) { - const device = this.getDevice(deviceId) || this.addPlejdDevice({ id: deviceId }); device.state = state; if (dim && device.dimmable) { device.dim = dim; diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index 5272249..d3a485c 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -10,15 +10,33 @@ const API_LOGIN_URL = 'login'; const API_SITE_LIST_URL = 'functions/getSiteList'; const API_SITE_DETAILS_URL = 'functions/getSiteById'; +const TRAITS = { + NO_LOAD: 0, + NON_DIMMABLE: 9, + DIMMABLE: 11, +}; + const logger = Logger.getLogger('plejd-api'); class PlejdApi { + /** @private @type {import('types/Configuration').Options} */ config; + + /** @private @type {import('DeviceRegistry')} */ deviceRegistry; + + /** @private @type {string} */ sessionToken; + + /** @private @type {string} */ siteId; + + /** @private @type {import('types/ApiSite').ApiSite} */ siteDetails; + /** + * @param {import("./DeviceRegistry")} deviceRegistry + */ constructor(deviceRegistry) { this.config = Configuration.getOptions(); this.deviceRegistry = deviceRegistry; @@ -58,19 +76,19 @@ class PlejdApi { } } } - this.deviceRegistry.setApiSite(this.siteDetails); this.deviceRegistry.cryptoKey = this.siteDetails.plejdMesh.cryptoKey; this.getDevices(); } + /** @returns {Promise} */ // eslint-disable-next-line class-methods-use-this async getCachedCopy() { logger.info('Getting cached api response from disk'); try { const rawData = await fs.promises.readFile('/data/cachedApiResponse.json'); - const cachedCopy = JSON.parse(rawData); + const cachedCopy = JSON.parse(rawData.toString()); return cachedCopy; } catch (err) { @@ -82,12 +100,14 @@ class PlejdApi { async saveCachedCopy() { logger.info('Saving cached copy'); try { - const rawData = JSON.stringify({ + /** @type {import('types/ApiSite').CachedSite} */ + const cachedSite = { siteId: this.siteId, siteDetails: this.siteDetails, sessionToken: this.sessionToken, dtCache: new Date().toISOString(), - }); + }; + const rawData = JSON.stringify(cachedSite); await fs.promises.writeFile('/data/cachedApiResponse.json', rawData); } catch (err) { logger.error('Failed to save cache of api response', err); @@ -194,6 +214,14 @@ class PlejdApi { getDevices() { logger.info('Getting devices from site details response...'); + if (this.siteDetails.gateways && this.siteDetails.gateways.length) { + this.siteDetails.gateways.forEach((gwy) => { + logger.info(`Plejd gateway '${gwy.title}' found on site`); + }); + } else { + logger.info('No Plejd gateway found on site'); + } + this._getPlejdDevices(); this._getRoomDevices(); this._getSceneDevices(); @@ -216,8 +244,11 @@ class PlejdApi { } // eslint-disable-next-line class-methods-use-this - _getDeviceType(hardwareId) { - switch (parseInt(hardwareId, 10)) { + _getDeviceType(plejdDevice) { + // Type name is also sometimes available in device.hardware.name + // (maybe only when GWY-01 is present?) + + switch (parseInt(plejdDevice.hardwareId, 10)) { case 1: case 11: return { name: 'DIM-01', type: 'light', dimmable: true }; @@ -259,69 +290,106 @@ class PlejdApi { case 20: return { name: 'SPR-01', type: 'switch', dimmable: false }; default: - throw new Error(`Unknown device type with id ${hardwareId}`); + throw new Error(`Unknown device type with id ${plejdDevice.hardwareId}`); } } + /** + * Plejd API properties parsed + * + * * `devices` - physical Plejd devices, duplicated for devices with multiple outputs + * devices: [{deviceId, title, objectId, ...}, {...}] + * * `deviceAddress` - BLE address of each physical device + * deviceAddress: {[deviceId]: bleDeviceId} + * * `outputSettings` - lots of info about load settings, also links devices to output index + * outputSettings: [{deviceId, output, deviceParseId, ...}] //deviceParseId === objectId above + * * `outputAddress`: BLE address of [0] main output and [n] other output (loads) + * outputAddress: {[deviceId]: {[output]: bleDeviceId}} + * * `inputSettings` - detailed settings for inputs (buttons, RTR-01, ...), scenes triggered, ... + * inputSettings: [{deviceId, input, ...}] //deviceParseId === objectId above + * * `inputAddress` - Links inputs to what BLE device they control, or 255 for unassigned/scene + * inputAddress: {[deviceId]: {[input]: bleDeviceId}} + */ _getPlejdDevices() { this.deviceRegistry.clearPlejdDevices(); this.siteDetails.devices.forEach((device) => { - const { deviceId } = device; - - const settings = this.siteDetails.outputSettings.find( + const outputSettings = this.siteDetails.outputSettings.find( (x) => x.deviceParseId === device.objectId, ); - let deviceNum = this.siteDetails.deviceAddress[deviceId]; + if (device.traits === TRAITS.NO_LOAD) { + logger.warn( + `Device ${device.title} (${device.deviceId}) has no load configured and will be excluded`, + ); + } else if (outputSettings) { + const uniqueOutputId = this.deviceRegistry.getUniqueOutputId( + device.deviceId, + outputSettings.output, + ); - if (settings) { - const outputs = this.siteDetails.outputAddress[deviceId]; - deviceNum = outputs[settings.output]; - } + const bleDeviceIndex = this.siteDetails.outputAddress[device.deviceId][ + outputSettings.output + ]; - // check if device is dimmable - const plejdDevice = this.siteDetails.plejdDevices.find((x) => x.deviceId === deviceId); - const deviceType = this._getDeviceType(plejdDevice.hardwareId); - const { name, type } = deviceType; - let { dimmable } = deviceType; + const plejdDevice = this.siteDetails.plejdDevices.find( + (x) => x.deviceId === device.deviceId, + ); - if (settings) { - dimmable = settings.dimCurve !== 'NonDimmable'; - } + const dimmable = device.traits === TRAITS.DIMMABLE; + // dimmable = settings.dimCurve !== 'NonDimmable'; - const newDevice = { - id: deviceNum, - name: device.title, - type, - typeName: name, - dimmable, - roomId: device.roomId, - version: plejdDevice.firmware.version, - serialNumber: plejdDevice.deviceId, - }; + const { name: typeName, type } = this._getDeviceType(plejdDevice); - if (newDevice.typeName === 'WPH-01') { - // WPH-01 is special, it has two buttons which needs to be - // registered separately. - const inputs = this.siteDetails.inputAddress[deviceId]; - const first = inputs[0]; - const second = inputs[1]; + /** @type {import('types/DeviceRegistry').OutputDevice} */ + const outputDevice = { + bleDeviceIndex, + deviceId: device.deviceId, + dimmable, + hiddenFromRoomList: device.hiddenFromRoomList, + hiddenFromIntegrations: device.hiddenFromIntegrations, + name: device.title, + output: outputSettings.output, + roomId: device.roomId, + state: undefined, + type, + typeName, + version: plejdDevice.firmware.version, + uniqueId: uniqueOutputId, + }; - this.deviceRegistry.addPlejdDevice({ - ...newDevice, - id: first, - name: `${device.title} left`, - }); - - this.deviceRegistry.addPlejdDevice({ - ...newDevice, - id: second, - name: `${device.title} right`, - }); + this.deviceRegistry.addOutputDevice(outputDevice); } else { - this.deviceRegistry.addPlejdDevice(newDevice); + logger.warn( + `No outputSettings found for ${device.title} (${device.deviceId}), device will not be included`, + ); + logger.verbose( + 'Fallback cound potentially be implemented by assuming default deviceSettings[deviceId]', + ); } + + // What should we do with inputs?! + // if (outputDevice.typeName === 'WPH-01') { + // // WPH-01 is special, it has two buttons which needs to be + // // registered separately. + // const inputs = this.siteDetails.inputAddress[deviceId]; + // const first = inputs[0]; + // const second = inputs[1]; + + // this.deviceRegistry.addPlejdDevice({ + // ...outputDevice, + // id: first, + // name: `${device.title} left`, + // }); + + // this.deviceRegistry.addPlejdDevice({ + // ...outputDevice, + // id: second, + // name: `${device.title} right`, + // }); + // } else { + // this.deviceRegistry.addPlejdDevice(outputDevice); + // } }); } @@ -332,20 +400,31 @@ class PlejdApi { const { roomId } = room; const roomAddress = this.siteDetails.roomAddress[roomId]; - const deviceIdsByRoom = this.deviceRegistry.getDeviceIdsByRoom(roomId); + const deviceIdsByRoom = this.deviceRegistry.getOutputDeviceIdsByRoomId(roomId); const dimmable = deviceIdsByRoom - && deviceIdsByRoom.some((deviceId) => this.deviceRegistry.getDevice(deviceId).dimmable); + && deviceIdsByRoom.some( + (deviceId) => this.deviceRegistry.getOutputDevice(deviceId).dimmable, + ); + /** @type {import('types/DeviceRegistry').OutputDevice} */ const newDevice = { - id: roomAddress, + bleDeviceIndex: roomAddress, + deviceId: null, + dimmable, + hiddenFromRoomList: false, + hiddenFromIntegrations: false, name: room.title, + output: undefined, + roomId, + state: undefined, type: 'light', typeName: 'Room', - dimmable, + uniqueId: roomId, + version: undefined, }; - this.deviceRegistry.addRoomDevice(newDevice); + this.deviceRegistry.addOutputDevice(newDevice); }); logger.debug('includeRoomsAsLights done.'); } @@ -357,14 +436,20 @@ class PlejdApi { scenes.forEach((scene) => { const sceneNum = this.siteDetails.sceneIndex[scene.sceneId]; + /** @type {import('types/DeviceRegistry').OutputDevice} */ const newScene = { - id: sceneNum, + bleDeviceIndex: sceneNum, + deviceId: undefined, + dimmable: false, + hiddenFromSceneList: scene.hiddenFromSceneList, name: scene.title, + output: undefined, + roomId: undefined, + state: false, type: 'switch', typeName: 'Scene', - dimmable: false, - version: '1.0', - serialNumber: scene.objectId, + version: undefined, + uniqueId: scene.sceneId, }; this.deviceRegistry.addScene(newScene); diff --git a/plejd/PlejdDeviceCommunication.js b/plejd/PlejdDeviceCommunication.js index e64c8cd..2fa4e2a 100644 --- a/plejd/PlejdDeviceCommunication.js +++ b/plejd/PlejdDeviceCommunication.js @@ -1,4 +1,4 @@ -const EventEmitter = require('events'); +const { EventEmitter } = require('events'); const Configuration = require('./Configuration'); const constants = require('./constants'); const Logger = require('./Logger'); @@ -15,6 +15,7 @@ class PlejdDeviceCommunication extends EventEmitter { bleDeviceTransitionTimers = {}; plejdBleHandler; config; + /** @type {import('./DeviceRegistry')} */ deviceRegistry; writeQueue = []; writeQueueRef = null; @@ -71,7 +72,7 @@ class PlejdDeviceCommunication extends EventEmitter { } turnOn(deviceId, command) { - const deviceName = this.deviceRegistry.getDeviceName(deviceId); + const deviceName = this.deviceRegistry.getOutputDeviceName(deviceId); logger.info( `Plejd got turn on command for ${deviceName} (${deviceId}), brightness ${command.brightness}${ command.transition ? `, transition: ${command.transition}` : '' @@ -81,7 +82,7 @@ class PlejdDeviceCommunication extends EventEmitter { } turnOff(deviceId, command) { - const deviceName = this.deviceRegistry.getDeviceName(deviceId); + const deviceName = this.deviceRegistry.getOutputDeviceName(deviceId); logger.info( `Plejd got turn off command for ${deviceName} (${deviceId})${ command.transition ? `, transition: ${command.transition}` : '' @@ -93,18 +94,18 @@ class PlejdDeviceCommunication extends EventEmitter { _bleCommandReceived(deviceId, command, data) { try { if (command === COMMANDS.DIM) { - this.deviceRegistry.setState(deviceId, data.state, data.dim); + this.deviceRegistry.setOutputState(deviceId, data.state, data.dim); this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, { - state: data.state, + state: !!data.state, brightness: data.dim, }); } else if (command === COMMANDS.TURN_ON) { - this.deviceRegistry.setState(deviceId, 1); + this.deviceRegistry.setOutputState(deviceId, true); this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, { state: 1, }); } else if (command === COMMANDS.TURN_OFF) { - this.deviceRegistry.setState(deviceId, 0); + this.deviceRegistry.setOutputState(deviceId, false); this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, { state: 0, }); @@ -125,11 +126,11 @@ class PlejdDeviceCommunication extends EventEmitter { } _transitionTo(deviceId, targetBrightness, transition, deviceName) { - const device = this.deviceRegistry.getDevice(deviceId); + const device = this.deviceRegistry.getOutputDevice(deviceId); const initialBrightness = device ? device.state && device.dim : null; this._clearDeviceTransitionTimer(deviceId); - const isDimmable = this.deviceRegistry.getDevice(deviceId).dimmable; + const isDimmable = this.deviceRegistry.getOutputDevice(deviceId).dimmable; if ( transition > 1 @@ -249,7 +250,7 @@ class PlejdDeviceCommunication extends EventEmitter { return; } const queueItem = this.writeQueue.pop(); - const deviceName = this.deviceRegistry.getDeviceName(queueItem.deviceId); + const deviceName = this.deviceRegistry.getOutputDeviceName(queueItem.deviceId); logger.debug( `Write queue: Processing ${deviceName} (${queueItem.deviceId}). Command ${ queueItem.command From ef718cf1dbf7f0b92fe68a910452ac8437044b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Wed, 31 Mar 2021 19:57:25 +0200 Subject: [PATCH 03/40] Add physical devices to DeviceRegistry to allow for serial number lookup during BLE startup --- plejd/DeviceRegistry.js | 88 +++++++++++++++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/plejd/DeviceRegistry.js b/plejd/DeviceRegistry.js index e1fb753..5ea12ba 100644 --- a/plejd/DeviceRegistry.js +++ b/plejd/DeviceRegistry.js @@ -5,18 +5,25 @@ class DeviceRegistry { /** @type {string} */ cryptoKey = null; - outputDeviceIdByRoomId = {}; - outputDeviceIdByBLEIndex = {}; + /** @private @type {Object.} */ + devices = {}; + /** @private */ + outputDeviceUniqueIdsByRoomId = {}; + /** @private */ + outputUniqueIdByBleOutputAddress = {}; + + /** @private @type {import('./types/ApiSite').ApiSite} */ + apiSite; // Dictionaries of [id]: device per type - /** @type {import('types/DeviceRegistry').OutputDevices} */ + /** @private @type {import('types/DeviceRegistry').OutputDevices} */ outputDevices = {}; - /** @type {import('types/DeviceRegistry').OutputDevices} */ + /** @private @type {import('types/DeviceRegistry').OutputDevices} */ sceneDevices = {}; - // eslint-disable-next-line class-methods-use-this - getUniqueOutputId(deviceId, outputIndex) { - return `${deviceId}_${outputIndex}`; + /** @param device {import('./types/ApiSite').Device} */ + addPhysicalDevice(device) { + this.devices[device.deviceId] = device; } /** @param outputDevice {import('types/DeviceRegistry').OutputDevice} */ @@ -32,19 +39,19 @@ class DeviceRegistry { } output devices in total.`, ); - this.outputDeviceIdByBLEIndex[outputDevice.bleDeviceIndex] = outputDevice.uniqueId; + this.outputUniqueIdByBleOutputAddress[outputDevice.bleOutputAddress] = outputDevice.uniqueId; - if (!this.outputDeviceIdByRoomId[outputDevice.roomId]) { - this.outputDeviceIdByRoomId[outputDevice.roomId] = []; + if (!this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId]) { + this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId] = []; } if ( - outputDevice.roomId !== outputDevice.uniqueId - && !this.outputDeviceIdByRoomId[outputDevice.roomId].includes(outputDevice.roomId) + outputDevice.roomId !== outputDevice.uniqueId && + !this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].includes(outputDevice.uniqueId) ) { - this.outputDeviceIdByRoomId[outputDevice.roomId].push(outputDevice.roomId); + this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].push(outputDevice.uniqueId); logger.verbose( `Added device to room ${outputDevice.roomId}: ${JSON.stringify( - this.outputDeviceIdByRoomId[outputDevice.roomId], + this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId], )}`, ); } @@ -70,28 +77,64 @@ class DeviceRegistry { } clearPlejdDevices() { + this.devices = {}; this.outputDevices = {}; - this.outputDeviceIdByRoomId = {}; - this.deviceIdsBySerial = {}; + this.outputDeviceUniqueIdsByRoomId = {}; + this.outputUniqueIdByBleOutputAddress = {}; } clearSceneDevices() { this.sceneDevices = {}; } + /** + * @returns {import('./types/DeviceRegistry').OutputDevice[]} + */ + getAllOutputDevices() { + return Object.values(this.outputDevices); + } + + /** + * @returns {import('./types/DeviceRegistry').OutputDevice[]} + */ + getAllSceneDevices() { + return Object.values(this.sceneDevices); + } + + /** @returns {import('./types/ApiSite').ApiSite} */ + getApiSite() { + return this.apiSite; + } + + /** + * @param {string} uniqueOutputId + */ getOutputDevice(uniqueOutputId) { return this.outputDevices[uniqueOutputId]; } + /** @returns {import('./types/DeviceRegistry').OutputDevice} */ + getOutputDeviceByBleOutputAddress(bleOutputAddress) { + return this.outputDevices[this.outputUniqueIdByBleOutputAddress[bleOutputAddress]]; + } + /** @returns {string[]} */ getOutputDeviceIdsByRoomId(roomId) { - return this.outputDeviceIdByRoomId[roomId]; + return this.outputDeviceUniqueIdsByRoomId[roomId]; } getOutputDeviceName(uniqueOutputId) { return (this.outputDevices[uniqueOutputId] || {}).name; } + /** + * @param {string } deviceId The physical device serial number + * @return {import('./types/ApiSite').Device} + */ + getPhysicalDevice(deviceId) { + return this.devices[deviceId]; + } + getScene(sceneId) { return this.sceneDevices[sceneId]; } @@ -100,6 +143,17 @@ class DeviceRegistry { return (this.sceneDevices[sceneId] || {}).name; } + // eslint-disable-next-line class-methods-use-this + getUniqueOutputId(deviceId, outputIndex) { + return `${deviceId}_${outputIndex}`; + } + + /** @param apiSite {import('./types/ApiSite').ApiSite} */ + setApiSite(apiSite) { + this.apiSite = apiSite; + this.cryptoKey = apiSite.plejdMesh.cryptoKey; + } + /** * @param {string} uniqueOutputId * @param {boolean} state From 9a76a3ba5082cba3b32b36acabbfe30733115941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Wed, 31 Mar 2021 20:04:45 +0200 Subject: [PATCH 04/40] Refactor code to use plejd outputs rather than devices as main entity --- plejd/DeviceRegistry.js | 4 +- plejd/PlejdAddon.js | 88 ++++++++++++----------- plejd/PlejdApi.js | 19 ++--- plejd/PlejdBLEHandler.js | 63 +++++++++-------- plejd/PlejdDeviceCommunication.js | 113 ++++++++++++++++-------------- plejd/Scene.js | 18 ++--- plejd/SceneManager.js | 28 ++++---- plejd/SceneStep.js | 4 ++ plejd/types/DeviceRegistry.d.ts | 4 +- plejd/types/PlejdApi.d.ts | 2 +- 10 files changed, 185 insertions(+), 158 deletions(-) diff --git a/plejd/DeviceRegistry.js b/plejd/DeviceRegistry.js index 5ea12ba..d683ac3 100644 --- a/plejd/DeviceRegistry.js +++ b/plejd/DeviceRegistry.js @@ -45,8 +45,8 @@ class DeviceRegistry { this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId] = []; } if ( - outputDevice.roomId !== outputDevice.uniqueId && - !this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].includes(outputDevice.uniqueId) + outputDevice.roomId !== outputDevice.uniqueId + && !this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].includes(outputDevice.uniqueId) ) { this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].push(outputDevice.uniqueId); logger.verbose( diff --git a/plejd/PlejdAddon.js b/plejd/PlejdAddon.js index eb39c50..cdb3d60 100644 --- a/plejd/PlejdAddon.js +++ b/plejd/PlejdAddon.js @@ -65,57 +65,61 @@ class PlejdAddon extends EventEmitter { }); // subscribe to changes from HA - this.mqttClient.on(MqttClient.EVENTS.stateChanged, (device, command) => { - try { - const deviceId = device.id; + this.mqttClient.on( + MqttClient.EVENTS.stateChanged, + /** @param device {import('./types/DeviceRegistry').OutputDevice} */ + (device, command) => { + try { + const { uniqueId } = device; - if (device.typeName === 'Scene') { - // we're triggering a scene, lets do that and jump out. - // since scenes aren't "real" devices. - this.sceneManager.executeScene(device.id); - return; + if (device.typeName === 'Scene') { + // we're triggering a scene, lets do that and jump out. + // since scenes aren't "real" devices. + this.sceneManager.executeScene(uniqueId); + return; + } + + let state = false; + let commandObj = {}; + + if (typeof command === 'string') { + // switch command + state = command === 'ON'; + commandObj = { + state, + }; + + // since the switch doesn't get any updates on whether it's on or not, + // we fake this by directly send the updateState back to HA in order for + // it to change state. + this.mqttClient.updateOutputState(uniqueId, { + state, + }); + } else { + // eslint-disable-next-line prefer-destructuring + state = command.state === 'ON'; + commandObj = command; + } + + if (state) { + this.plejdDeviceCommunication.turnOn(uniqueId, commandObj); + } else { + this.plejdDeviceCommunication.turnOff(uniqueId, commandObj); + } + } catch (err) { + logger.error('Error in MqttClient.stateChanged callback', err); } - - let state = 'OFF'; - let commandObj = {}; - - if (typeof command === 'string') { - // switch command - state = command; - commandObj = { - state, - }; - - // since the switch doesn't get any updates on whether it's on or not, - // we fake this by directly send the updateState back to HA in order for - // it to change state. - this.mqttClient.updateState(deviceId, { - state: state === 'ON' ? 1 : 0, - }); - } else { - // eslint-disable-next-line prefer-destructuring - state = command.state; - commandObj = command; - } - - if (state === 'ON') { - this.plejdDeviceCommunication.turnOn(deviceId, commandObj); - } else { - this.plejdDeviceCommunication.turnOff(deviceId, commandObj); - } - } catch (err) { - logger.error('Error in MqttClient.stateChanged callback', err); - } - }); + }, + ); this.mqttClient.init(); // subscribe to changes from Plejd this.plejdDeviceCommunication.on( PlejdDeviceCommunication.EVENTS.stateChanged, - (deviceId, command) => { + (uniqueOutputId, command) => { try { - this.mqttClient.updateState(deviceId, command); + this.mqttClient.updateOutputState(uniqueOutputId, command); } catch (err) { logger.error('Error in PlejdService.stateChanged callback', err); } diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index d3a485c..01616c0 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -76,8 +76,8 @@ class PlejdApi { } } } - this.deviceRegistry.cryptoKey = this.siteDetails.plejdMesh.cryptoKey; + this.deviceRegistry.setApiSite(this.siteDetails); this.getDevices(); } @@ -300,20 +300,22 @@ class PlejdApi { * * `devices` - physical Plejd devices, duplicated for devices with multiple outputs * devices: [{deviceId, title, objectId, ...}, {...}] * * `deviceAddress` - BLE address of each physical device - * deviceAddress: {[deviceId]: bleDeviceId} + * deviceAddress: {[deviceId]: bleDeviceAddress} * * `outputSettings` - lots of info about load settings, also links devices to output index * outputSettings: [{deviceId, output, deviceParseId, ...}] //deviceParseId === objectId above * * `outputAddress`: BLE address of [0] main output and [n] other output (loads) - * outputAddress: {[deviceId]: {[output]: bleDeviceId}} + * outputAddress: {[deviceId]: {[output]: bleDeviceAddress}} * * `inputSettings` - detailed settings for inputs (buttons, RTR-01, ...), scenes triggered, ... * inputSettings: [{deviceId, input, ...}] //deviceParseId === objectId above * * `inputAddress` - Links inputs to what BLE device they control, or 255 for unassigned/scene - * inputAddress: {[deviceId]: {[input]: bleDeviceId}} + * inputAddress: {[deviceId]: {[input]: bleDeviceAddress}} */ _getPlejdDevices() { this.deviceRegistry.clearPlejdDevices(); this.siteDetails.devices.forEach((device) => { + this.deviceRegistry.addPhysicalDevice(device); + const outputSettings = this.siteDetails.outputSettings.find( (x) => x.deviceParseId === device.objectId, ); @@ -328,7 +330,7 @@ class PlejdApi { outputSettings.output, ); - const bleDeviceIndex = this.siteDetails.outputAddress[device.deviceId][ + const bleOutputAddress = this.siteDetails.outputAddress[device.deviceId][ outputSettings.output ]; @@ -343,7 +345,7 @@ class PlejdApi { /** @type {import('types/DeviceRegistry').OutputDevice} */ const outputDevice = { - bleDeviceIndex, + bleOutputAddress, deviceId: device.deviceId, dimmable, hiddenFromRoomList: device.hiddenFromRoomList, @@ -409,7 +411,7 @@ class PlejdApi { /** @type {import('types/DeviceRegistry').OutputDevice} */ const newDevice = { - bleDeviceIndex: roomAddress, + bleOutputAddress: roomAddress, deviceId: null, dimmable, hiddenFromRoomList: false, @@ -431,6 +433,7 @@ class PlejdApi { } _getSceneDevices() { + this.deviceRegistry.clearSceneDevices(); // add scenes as switches const scenes = this.siteDetails.scenes.filter((x) => x.hiddenFromSceneList === false); @@ -438,7 +441,7 @@ class PlejdApi { const sceneNum = this.siteDetails.sceneIndex[scene.sceneId]; /** @type {import('types/DeviceRegistry').OutputDevice} */ const newScene = { - bleDeviceIndex: sceneNum, + bleOutputAddress: sceneNum, deviceId: undefined, dimmable: false, hiddenFromSceneList: scene.hiddenFromSceneList, diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 84f1329..2bb062b 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -49,6 +49,8 @@ class PlejBLEHandler extends EventEmitter { connectedDevice = null; consecutiveWriteFails; consecutiveReconnectAttempts = 0; + /** @type {import('./DeviceRegistry')} */ + deviceRegistry; discoveryTimeout = null; plejdService = null; pingRef = null; @@ -152,21 +154,21 @@ class PlejBLEHandler extends EventEmitter { logger.info('BLE init done, waiting for devices.'); } - async sendCommand(command, deviceId, data) { + async sendCommand(command, uniqueOutputId, data) { let payload; let brightnessVal; switch (command) { case COMMANDS.TURN_ON: - payload = this._createHexPayload(deviceId, BLE_CMD_STATE_CHANGE, '01'); + payload = this._createHexPayload(uniqueOutputId, BLE_CMD_STATE_CHANGE, '01'); break; case COMMANDS.TURN_OFF: - payload = this._createHexPayload(deviceId, BLE_CMD_STATE_CHANGE, '00'); + payload = this._createHexPayload(uniqueOutputId, BLE_CMD_STATE_CHANGE, '00'); break; case COMMANDS.DIM: // eslint-disable-next-line no-bitwise brightnessVal = (data << 8) | data; payload = this._createHexPayload( - deviceId, + uniqueOutputId, BLE_CMD_DIM2_CHANGE, `01${brightnessVal.toString(16).padStart(4, '0')}`, ); @@ -194,9 +196,9 @@ class PlejBLEHandler extends EventEmitter { plejd.instance = device; const segments = plejd.path.split('/'); - let fixedPlejdPath = segments[segments.length - 1].replace('dev_', ''); - fixedPlejdPath = fixedPlejdPath.replace(/_/g, ''); - plejd.device = this.deviceRegistry.getDeviceBySerialNumber(fixedPlejdPath); + let plejdSerialNumber = segments[segments.length - 1].replace('dev_', ''); + plejdSerialNumber = plejdSerialNumber.replace(/_/g, ''); + plejd.device = this.deviceRegistry.getPhysicalDevice(plejdSerialNumber); if (plejd.device) { logger.debug( @@ -204,7 +206,7 @@ class PlejBLEHandler extends EventEmitter { ); this.bleDevices.push(plejd); } else { - logger.warn(`Device registry does not contain device with serial ${fixedPlejdPath}`); + logger.warn(`Device registry does not contain device with serial ${plejdSerialNumber}`); } } catch (err) { logger.error(`Failed inspecting ${path}. `, err); @@ -796,7 +798,7 @@ class PlejBLEHandler extends EventEmitter { return; } - const deviceId = decoded.readUInt8(0); + const bleOutputAddress = decoded.readUInt8(0); // Bytes 2-3 is Command/Request const cmd = decoded.readUInt16BE(3); @@ -810,38 +812,41 @@ class PlejBLEHandler extends EventEmitter { logger.silly(`Dim: ${dim.toString(16)}, full precision: ${dimFull.toString(16)}`); } - const deviceName = this.deviceRegistry.getDeviceName(deviceId); + const device = this.deviceRegistry.getOutputDeviceByBleOutputAddress(bleOutputAddress); + const deviceName = device ? device.name : 'Unknown'; + const outputUniqueId = device ? device.uniqueId : null; + if (Logger.shouldLog('verbose')) { // decoded.toString() could potentially be expensive logger.verbose(`Raw event received: ${decoded.toString('hex')}`); logger.verbose( - `Decoded: Device ${deviceId}, cmd ${cmd.toString(16)}, state ${state}, dim ${dim}`, + `Decoded: Device ${outputUniqueId}, cmd ${cmd.toString(16)}, state ${state}, dim ${dim}`, ); } let command; let data = {}; if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) { - logger.debug(`${deviceName} (${deviceId}) got state+dim update. S: ${state}, D: ${dim}`); + logger.debug( + `${deviceName} (${outputUniqueId}) got state+dim update. S: ${state}, D: ${dim}`, + ); command = COMMANDS.DIM; data = { state, dim }; - this.emit(PlejBLEHandler.EVENTS.commandReceived, deviceId, command, data); + this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data); } else if (cmd === BLE_CMD_STATE_CHANGE) { - logger.debug(`${deviceName} (${deviceId}) got state update. S: ${state}`); + logger.debug(`${deviceName} (${outputUniqueId}) got state update. S: ${state}`); command = state ? COMMANDS.TURN_ON : COMMANDS.TURN_OFF; - this.emit(PlejBLEHandler.EVENTS.commandReceived, deviceId, command, data); + this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data); } else if (cmd === BLE_CMD_SCENE_TRIG) { const sceneId = state; const sceneName = this.deviceRegistry.getSceneName(sceneId); - logger.debug( - `${sceneName} (${sceneId}) scene triggered (device id ${deviceId}). Name can be misleading if there is a device with the same numeric id.`, - ); + logger.debug(`${sceneName} (${sceneId}) scene triggered (device id ${outputUniqueId}).`); command = COMMANDS.TRIGGER_SCENE; data = { sceneId }; - this.emit(PlejBLEHandler.EVENTS.commandReceived, deviceId, command, data); + this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data); } else if (cmd === BLE_CMD_TIME_UPDATE) { const now = new Date(); // Guess Plejd timezone based on HA time zone @@ -851,7 +856,7 @@ class PlejBLEHandler extends EventEmitter { const plejdTimestampUTC = (decoded.readInt32LE(5) + offsetSecondsGuess) * 1000; const diffSeconds = Math.round((plejdTimestampUTC - now.getTime()) / 1000); if ( - deviceId !== BLE_BROADCAST_DEVICE_ID + bleOutputAddress !== BLE_BROADCAST_DEVICE_ID || Logger.shouldLog('verbose') || Math.abs(diffSeconds) > 60 ) { @@ -863,7 +868,7 @@ class PlejBLEHandler extends EventEmitter { logger.warn( `Plejd clock time off by more than 1 minute. Reported time: ${plejdTime.toString()}, diff ${diffSeconds} seconds. Time will be set hourly.`, ); - if (this.connectedDevice && deviceId === this.connectedDevice.id) { + if (this.connectedDevice && bleOutputAddress === this.connectedDevice.id) { // Requested time sync by us const newLocalTimestamp = now.getTime() / 1000 - offsetSecondsGuess; logger.info(`Setting time to ${now.toString()}`); @@ -881,7 +886,7 @@ class PlejBLEHandler extends EventEmitter { ); } } - } else if (deviceId !== BLE_BROADCAST_DEVICE_ID) { + } else if (bleOutputAddress !== BLE_BROADCAST_DEVICE_ID) { logger.info('Got time response. Plejd clock time in sync with Home Assistant time'); } } @@ -889,19 +894,19 @@ class PlejBLEHandler extends EventEmitter { logger.verbose( `Command ${cmd.toString(16)} unknown. ${decoded.toString( 'hex', - )}. Device ${deviceName} (${deviceId})`, + )}. Device ${deviceName} (${bleOutputAddress}: ${outputUniqueId})`, ); } } _createHexPayload( - deviceId, + bleOutputAddress, command, hexDataString, requestResponseCommand = BLE_REQUEST_NO_RESPONSE, ) { return this._createPayload( - deviceId, + bleOutputAddress, command, 5 + Math.ceil(hexDataString.length / 2), (payload) => payload.write(hexDataString, 5, 'hex'), @@ -911,14 +916,14 @@ class PlejBLEHandler extends EventEmitter { // eslint-disable-next-line class-methods-use-this _createPayload( - deviceId, + bleOutputAddress, command, bufferLength, payloadBufferAddDataFunc, requestResponseCommand = BLE_REQUEST_NO_RESPONSE, ) { const payload = Buffer.alloc(bufferLength); - payload.writeUInt8(deviceId); + payload.writeUInt8(bleOutputAddress); payload.writeUInt16BE(requestResponseCommand, 1); payload.writeUInt16BE(command, 3); payloadBufferAddDataFunc(payload); @@ -945,12 +950,12 @@ class PlejBLEHandler extends EventEmitter { let ct = cipher.update(buf).toString('hex'); ct += cipher.final().toString('hex'); - ct = Buffer.from(ct, 'hex'); + const ctBuf = Buffer.from(ct, 'hex'); let output = ''; for (let i = 0, { length } = data; i < length; i++) { // eslint-disable-next-line no-bitwise - output += String.fromCharCode(data[i] ^ ct[i % 16]); + output += String.fromCharCode(data[i] ^ ctBuf[i % 16]); } return Buffer.from(output, 'ascii'); diff --git a/plejd/PlejdDeviceCommunication.js b/plejd/PlejdDeviceCommunication.js index 2fa4e2a..3c4431b 100644 --- a/plejd/PlejdDeviceCommunication.js +++ b/plejd/PlejdDeviceCommunication.js @@ -12,11 +12,13 @@ const MAX_RETRY_COUNT = 10; // Could be made a setting class PlejdDeviceCommunication extends EventEmitter { bleConnected; - bleDeviceTransitionTimers = {}; + bleOutputTransitionTimers = {}; plejdBleHandler; config; /** @type {import('./DeviceRegistry')} */ deviceRegistry; + // eslint-disable-next-line max-len + /** @type {{uniqueOutputId: string, command: string, data: any, shouldRetry: boolean, retryCount?: number}[]} */ writeQueue = []; writeQueueRef = null; @@ -35,7 +37,7 @@ class PlejdDeviceCommunication extends EventEmitter { } cleanup() { - Object.values(this.bleDeviceTransitionTimers).forEach((t) => clearTimeout(t)); + Object.values(this.bleOutputTransitionTimers).forEach((t) => clearTimeout(t)); this.plejdBleHandler.cleanup(); this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.commandReceived); this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.connected); @@ -47,7 +49,10 @@ class PlejdDeviceCommunication extends EventEmitter { this.cleanup(); this.bleConnected = false; // eslint-disable-next-line max-len - this.plejdBleHandler.on(PlejBLEHandler.EVENTS.commandReceived, (deviceId, command, data) => this._bleCommandReceived(deviceId, command, data)); + this.plejdBleHandler.on( + PlejBLEHandler.EVENTS.commandReceived, + (uniqueOutputId, command, data) => this._bleCommandReceived(uniqueOutputId, command, data), + ); this.plejdBleHandler.on(PlejBLEHandler.EVENTS.connected, () => { logger.info('Bluetooth connected. Plejd BLE up and running!'); @@ -71,42 +76,42 @@ class PlejdDeviceCommunication extends EventEmitter { } } - turnOn(deviceId, command) { - const deviceName = this.deviceRegistry.getOutputDeviceName(deviceId); + turnOn(uniqueOutputId, command) { + const deviceName = this.deviceRegistry.getOutputDeviceName(uniqueOutputId); logger.info( - `Plejd got turn on command for ${deviceName} (${deviceId}), brightness ${command.brightness}${ + `Plejd got turn on command for ${deviceName} (${uniqueOutputId}), brightness ${ + command.brightness + }${command.transition ? `, transition: ${command.transition}` : ''}`, + ); + this._transitionTo(uniqueOutputId, command.brightness, command.transition, deviceName); + } + + turnOff(uniqueOutputId, command) { + const deviceName = this.deviceRegistry.getOutputDeviceName(uniqueOutputId); + logger.info( + `Plejd got turn off command for ${deviceName} (${uniqueOutputId})${ command.transition ? `, transition: ${command.transition}` : '' }`, ); - this._transitionTo(deviceId, command.brightness, command.transition, deviceName); + this._transitionTo(uniqueOutputId, 0, command.transition, deviceName); } - turnOff(deviceId, command) { - const deviceName = this.deviceRegistry.getOutputDeviceName(deviceId); - logger.info( - `Plejd got turn off command for ${deviceName} (${deviceId})${ - command.transition ? `, transition: ${command.transition}` : '' - }`, - ); - this._transitionTo(deviceId, 0, command.transition, deviceName); - } - - _bleCommandReceived(deviceId, command, data) { + _bleCommandReceived(uniqueOutputId, command, data) { try { if (command === COMMANDS.DIM) { - this.deviceRegistry.setOutputState(deviceId, data.state, data.dim); - this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, { + this.deviceRegistry.setOutputState(uniqueOutputId, data.state, data.dim); + this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, { state: !!data.state, brightness: data.dim, }); } else if (command === COMMANDS.TURN_ON) { - this.deviceRegistry.setOutputState(deviceId, true); - this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, { + this.deviceRegistry.setOutputState(uniqueOutputId, true); + this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, { state: 1, }); } else if (command === COMMANDS.TURN_OFF) { - this.deviceRegistry.setOutputState(deviceId, false); - this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, { + this.deviceRegistry.setOutputState(uniqueOutputId, false); + this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, { state: 0, }); } else if (command === COMMANDS.TRIGGER_SCENE) { @@ -119,18 +124,18 @@ class PlejdDeviceCommunication extends EventEmitter { } } - _clearDeviceTransitionTimer(deviceId) { - if (this.bleDeviceTransitionTimers[deviceId]) { - clearInterval(this.bleDeviceTransitionTimers[deviceId]); + _clearDeviceTransitionTimer(uniqueOutputId) { + if (this.bleOutputTransitionTimers[uniqueOutputId]) { + clearInterval(this.bleOutputTransitionTimers[uniqueOutputId]); } } - _transitionTo(deviceId, targetBrightness, transition, deviceName) { - const device = this.deviceRegistry.getOutputDevice(deviceId); + _transitionTo(uniqueOutputId, targetBrightness, transition, deviceName) { + const device = this.deviceRegistry.getOutputDevice(uniqueOutputId); const initialBrightness = device ? device.state && device.dim : null; - this._clearDeviceTransitionTimer(deviceId); + this._clearDeviceTransitionTimer(uniqueOutputId); - const isDimmable = this.deviceRegistry.getOutputDevice(deviceId).dimmable; + const isDimmable = this.deviceRegistry.getOutputDevice(uniqueOutputId).dimmable; if ( transition > 1 @@ -165,7 +170,7 @@ class PlejdDeviceCommunication extends EventEmitter { let nSteps = 0; - this.bleDeviceTransitionTimers[deviceId] = setInterval(() => { + this.bleOutputTransitionTimers[uniqueOutputId] = setInterval(() => { const tElapsedMs = new Date().getTime() - dtStart.getTime(); let tElapsed = tElapsedMs / 1000; @@ -179,20 +184,20 @@ class PlejdDeviceCommunication extends EventEmitter { if (tElapsed === transition) { nSteps++; - this._clearDeviceTransitionTimer(deviceId); + this._clearDeviceTransitionTimer(uniqueOutputId); newBrightness = targetBrightness; logger.debug( - `Queueing finalize ${deviceName} (${deviceId}) transition from ${initialBrightness} to ${targetBrightness} in ${tElapsedMs}ms. Done steps ${nSteps}. Average interval ${ + `Queueing finalize ${deviceName} (${uniqueOutputId}) transition from ${initialBrightness} to ${targetBrightness} in ${tElapsedMs}ms. Done steps ${nSteps}. Average interval ${ tElapsedMs / (nSteps || 1) } ms.`, ); - this._setBrightness(deviceId, newBrightness, true, deviceName); + this._setBrightness(uniqueOutputId, newBrightness, true, deviceName); } else { nSteps++; logger.verbose( - `Queueing dim transition for ${deviceName} (${deviceId}) to ${newBrightness}. Total queue length ${this.writeQueue.length}`, + `Queueing dim transition for ${deviceName} (${uniqueOutputId}) to ${newBrightness}. Total queue length ${this.writeQueue.length}`, ); - this._setBrightness(deviceId, newBrightness, false, deviceName); + this._setBrightness(uniqueOutputId, newBrightness, false, deviceName); } }, transitionInterval); } else { @@ -201,34 +206,34 @@ class PlejdDeviceCommunication extends EventEmitter { `Could not transition light change. Either initial value is unknown or change is too small. Requested from ${initialBrightness} to ${targetBrightness}`, ); } - this._setBrightness(deviceId, targetBrightness, true, deviceName); + this._setBrightness(uniqueOutputId, targetBrightness, true, deviceName); } } - _setBrightness(deviceId, brightness, shouldRetry, deviceName) { + _setBrightness(unqiueOutputId, brightness, shouldRetry, deviceName) { if (!brightness && brightness !== 0) { logger.debug( - `Queueing turn on ${deviceName} (${deviceId}). No brightness specified, setting DIM to previous.`, + `Queueing turn on ${deviceName} (${unqiueOutputId}). No brightness specified, setting DIM to previous.`, ); - this._appendCommandToWriteQueue(deviceId, COMMANDS.TURN_ON, null, shouldRetry); + this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.TURN_ON, null, shouldRetry); } else if (brightness <= 0) { - logger.debug(`Queueing turn off ${deviceId}`); - this._appendCommandToWriteQueue(deviceId, COMMANDS.TURN_OFF, null, shouldRetry); + logger.debug(`Queueing turn off ${unqiueOutputId}`); + this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.TURN_OFF, null, shouldRetry); } else { if (brightness > 255) { // eslint-disable-next-line no-param-reassign brightness = 255; } - logger.debug(`Queueing ${deviceId} set brightness to ${brightness}`); + logger.debug(`Queueing ${unqiueOutputId} set brightness to ${brightness}`); // eslint-disable-next-line no-bitwise - this._appendCommandToWriteQueue(deviceId, COMMANDS.DIM, brightness, shouldRetry); + this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.DIM, brightness, shouldRetry); } } - _appendCommandToWriteQueue(deviceId, command, data, shouldRetry) { + _appendCommandToWriteQueue(uniqueOutputId, command, data, shouldRetry) { this.writeQueue.unshift({ - deviceId, + uniqueOutputId, command, data, shouldRetry, @@ -250,28 +255,28 @@ class PlejdDeviceCommunication extends EventEmitter { return; } const queueItem = this.writeQueue.pop(); - const deviceName = this.deviceRegistry.getOutputDeviceName(queueItem.deviceId); + const deviceName = this.deviceRegistry.getOutputDeviceName(queueItem.uniqueOutputId); logger.debug( - `Write queue: Processing ${deviceName} (${queueItem.deviceId}). Command ${ + `Write queue: Processing ${deviceName} (${queueItem.uniqueOutputId}). Command ${ queueItem.command }${queueItem.data ? ` ${queueItem.data}` : ''}. Total queue length: ${ this.writeQueue.length }`, ); - if (this.writeQueue.some((item) => item.deviceId === queueItem.deviceId)) { + if (this.writeQueue.some((item) => item.uniqueOutputId === queueItem.uniqueOutputId)) { logger.verbose( - `Skipping ${deviceName} (${queueItem.deviceId}) ` + `Skipping ${deviceName} (${queueItem.uniqueOutputId}) ` + `${queueItem.command} due to more recent command in queue.`, ); - // Skip commands if new ones exist for the same deviceId + // Skip commands if new ones exist for the same uniqueOutputId // still process all messages in order } else { /* eslint-disable no-await-in-loop */ try { await this.plejdBleHandler.sendCommand( queueItem.command, - queueItem.deviceId, + queueItem.uniqueOutputId, queueItem.data, ); } catch (err) { @@ -282,7 +287,7 @@ class PlejdDeviceCommunication extends EventEmitter { this.writeQueue.push(queueItem); // Add back to top of queue to be processed next; } else { logger.error( - `Write queue: Exceeed max retry count (${MAX_RETRY_COUNT}) for ${deviceName} (${queueItem.deviceId}). Command ${queueItem.command} failed.`, + `Write queue: Exceeed max retry count (${MAX_RETRY_COUNT}) for ${deviceName} (${queueItem.uniqueOutputId}). Command ${queueItem.command} failed.`, ); break; } diff --git a/plejd/Scene.js b/plejd/Scene.js index e0bff71..b22f304 100644 --- a/plejd/Scene.js +++ b/plejd/Scene.js @@ -1,18 +1,20 @@ const SceneStep = require('./SceneStep'); class Scene { - constructor(idx, scene, steps) { + /** + * @param {import('./DeviceRegistry')} deviceRegistry + * @param {number} idx + * @param {import("./types/ApiSite").Scene} scene + */ + constructor(deviceRegistry, idx, scene) { this.id = idx; this.title = scene.title; this.sceneId = scene.sceneId; - const sceneSteps = steps.filter((x) => x.sceneId === scene.sceneId); - this.steps = []; - - // eslint-disable-next-line no-restricted-syntax - for (const step of sceneSteps) { - this.steps.push(new SceneStep(step)); - } + this.steps = deviceRegistry + .getApiSite() + .sceneSteps.filter((step) => step.sceneId === scene.sceneId) + .map((step) => new SceneStep(step)); } } diff --git a/plejd/SceneManager.js b/plejd/SceneManager.js index e5047cc..4f518ad 100644 --- a/plejd/SceneManager.js +++ b/plejd/SceneManager.js @@ -3,25 +3,28 @@ const Scene = require('./Scene'); const logger = Logger.getLogger('scene-manager'); class SceneManager { + /** @private @type {import('./DeviceRegistry')} */ deviceRegistry; - plejdBle; + /** @private @type {import('./PlejdDeviceCommunication')} */ + plejdDeviceCommunication; + /** @private @type {Object.} */ scenes; - constructor(deviceRegistry, plejdBle) { + constructor(deviceRegistry, plejdDeviceCommunication) { this.deviceRegistry = deviceRegistry; - this.plejdBle = plejdBle; + this.plejdDeviceCommunication = plejdDeviceCommunication; this.scenes = {}; } init() { - const scenes = this.deviceRegistry.apiSite.scenes.filter( - (x) => x.hiddenFromSceneList === false, - ); + const scenes = this.deviceRegistry + .getApiSite() + .scenes.filter((x) => x.hiddenFromSceneList === false); this.scenes = {}; scenes.forEach((scene) => { - const idx = this.deviceRegistry.apiSite.sceneIndex[scene.sceneId]; - this.scenes[idx] = new Scene(idx, scene, this.deviceRegistry.apiSite.sceneSteps); + const idx = this.deviceRegistry.getApiSite().sceneIndex[scene.sceneId]; + this.scenes[idx] = new Scene(this.deviceRegistry, idx, scene); }); } @@ -34,14 +37,15 @@ class SceneManager { } scene.steps.forEach((step) => { - const device = this.deviceRegistry.getDeviceBySerialNumber(step.deviceId); + const uniqueId = this.deviceRegistry.getUniqueOutputId(step.deviceId, step.output); + const device = this.deviceRegistry.getOutputDevice(uniqueId); if (device) { if (device.dimmable && step.state) { - this.plejdBle.turnOn(device.id, { brightness: step.brightness }); + this.plejdDeviceCommunication.turnOn(uniqueId, { brightness: step.brightness }); } else if (!device.dimmable && step.state) { - this.plejdBle.turnOn(device.id, {}); + this.plejdDeviceCommunication.turnOn(uniqueId, {}); } else if (!step.state) { - this.plejdBle.turnOff(device.id, {}); + this.plejdDeviceCommunication.turnOff(uniqueId, {}); } } }); diff --git a/plejd/SceneStep.js b/plejd/SceneStep.js index 6456997..3ccc06f 100644 --- a/plejd/SceneStep.js +++ b/plejd/SceneStep.js @@ -1,7 +1,11 @@ class SceneStep { + /** + * @param {import("./types/ApiSite").SceneStep} step + */ constructor(step) { this.sceneId = step.sceneId; this.deviceId = step.deviceId; + this.output = step.output; this.state = step.state === 'On' ? 1 : 0; this.brightness = step.value; } diff --git a/plejd/types/DeviceRegistry.d.ts b/plejd/types/DeviceRegistry.d.ts index 4dc373f..b93c509 100644 --- a/plejd/types/DeviceRegistry.d.ts +++ b/plejd/types/DeviceRegistry.d.ts @@ -3,7 +3,7 @@ export type OutputDevices = { [deviceIdAndOutput: string]: OutputDevice }; export interface OutputDevice { - bleDeviceIndex: number; + bleOutputAddress: number; deviceId: string; dim?: number; dimmable: boolean; @@ -13,7 +13,7 @@ export interface OutputDevice { name: string; output: number; roomId: string; - state: number | undefined; + state: boolean | undefined; type: string; typeName: string; version: string; diff --git a/plejd/types/PlejdApi.d.ts b/plejd/types/PlejdApi.d.ts index 1cbdbe8..3376b65 100644 --- a/plejd/types/PlejdApi.d.ts +++ b/plejd/types/PlejdApi.d.ts @@ -1,6 +1,6 @@ /* eslint-disable no-use-before-define */ -import { ApiSite } from './ApiSite'; +import { ApiSite } from './ApiSite.d.ts'; export type PlejdApi = { config: any; From 7de1238c12718a2cc13424775a3743063e219a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Wed, 31 Mar 2021 20:07:46 +0200 Subject: [PATCH 05/40] Refactor mqtt messaging to use unique id:s and trigger scenes rather than switches --- plejd/MqttClient.js | 126 ++++++++++++++++++++++++++++++-------------- 1 file changed, 87 insertions(+), 39 deletions(-) diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index 0369010..af79c03 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -13,12 +13,22 @@ const logger = Logger.getLogger('plejd-mqtt'); const discoveryPrefix = 'homeassistant'; const nodeId = 'plejd'; +const getMqttUniqueId = (/** @type {string} */ uniqueId) => `${nodeId}.${uniqueId}`; + const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`; -const getPath = ({ id, type }) => `${discoveryPrefix}/${type}/${nodeId}/${id}`; -const getConfigPath = (plug) => `${getPath(plug)}/config`; -const getStateTopic = (plug) => `${getPath(plug)}/state`; -const getAvailabilityTopic = (plug) => `${getPath(plug)}/availability`; -const getCommandTopic = (plug) => `${getPath(plug)}/set`; +const getBaseTopic = (/** @type {{ uniqueId: string; type: string; }} */ plug) => `${discoveryPrefix}/${plug.type}/${nodeId}/${getMqttUniqueId(plug.uniqueId)}`; + +const getTopicName = ( + /** @type {{ uniqueId: string; type: string; }} */ plug, + /** @type {'config' | 'state' | 'availability' | 'set'} */ topicType, +) => `${getBaseTopic(plug)}/${topicType}`; + +const TOPICS = { + CONFIG: 'config', + STATE: 'state', + AVAILABILITY: 'availability', + COMMAND: 'set', +}; const getSceneEventTopic = () => 'plejd/event/scene'; const decodeTopicRegexp = new RegExp( @@ -33,17 +43,22 @@ const decodeTopic = (topic) => { return matches.groups; }; -const getDiscoveryPayload = (device) => ({ +const getLightDiscoveryPayload = ( + /** @type {import('./types/DeviceRegistry').OutputDevice} */ device, +) => ({ schema: 'json', name: device.name, - unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`, - state_topic: getStateTopic(device), - command_topic: getCommandTopic(device), - availability_topic: getAvailabilityTopic(device), + unique_id: getMqttUniqueId(device.uniqueId), + '~': getBaseTopic(device), + state_topic: `~/${TOPICS.STATE}`, + command_topic: `~/${TOPICS.COMMAND}`, + availability_topic: `~/${TOPICS.AVAILABILITY}`, optimistic: false, - brightness: `${device.dimmable}`, + qos: 1, + retain: true, + brightness: device.dimmable, device: { - identifiers: `${device.serialNumber}_${device.id}`, + identifiers: `${device.deviceId}`, manufacturer: 'Plejd', model: device.typeName, name: device.name, @@ -51,23 +66,32 @@ const getDiscoveryPayload = (device) => ({ }, }); -const getSwitchPayload = (device) => ({ - name: device.name, - state_topic: getStateTopic(device), - command_topic: getCommandTopic(device), +const getScenehDiscoveryPayload = ( + /** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice, +) => ({ + name: sceneDevice.name, + '~': getBaseTopic(sceneDevice), + state_topic: `~/${TOPICS.STATE}`, + command_topic: `~/${TOPICS.COMMAND}`, optimistic: false, + qos: 1, + retain: true, device: { - identifiers: `${device.serialNumber}_${device.id}`, + identifiers: `${sceneDevice.uniqueId}`, manufacturer: 'Plejd', - model: device.typeName, - name: device.name, - sw_version: device.version, + model: sceneDevice.typeName, + name: sceneDevice.name, + sw_version: sceneDevice.version, }, }); // #endregion +const getMqttStateString = (/** @type {boolean} */ state) => (state ? 'ON' : 'OFF'); +const AVAILABLILITY = { ONLINE: 'online', OFFLINE: 'offline' }; + class MqttClient extends EventEmitter { + /** @type {import('DeviceRegistry')} */ deviceRegistry; static EVENTS = { @@ -125,7 +149,7 @@ class MqttClient extends EventEmitter { } else { const decodedTopic = decodeTopic(topic); if (decodedTopic) { - let device = this.deviceRegistry.getDevice(decodedTopic.id); + let device = this.deviceRegistry.getOutputDevice(decodedTopic.id); const messageString = message.toString(); const isJsonMessage = messageString.startsWith('{'); @@ -144,6 +168,7 @@ class MqttClient extends EventEmitter { ); device = this.deviceRegistry.getScene(decodedTopic.id); } + const deviceName = device ? device.name : ''; switch (decodedTopic.command) { @@ -195,35 +220,55 @@ class MqttClient extends EventEmitter { } disconnect(callback) { - this.deviceRegistry.allDevices.forEach((device) => { - this.client.publish(getAvailabilityTopic(device), 'offline'); + this.deviceRegistry.getAllOutputDevices().forEach((outputDevice) => { + this.client.publish(getTopicName(outputDevice, 'availability'), AVAILABLILITY.OFFLINE); }); this.client.end(callback); } sendDiscoveryToHomeAssistant() { - logger.debug(`Sending discovery of ${this.deviceRegistry.allDevices.length} device(s).`); + const allOutputDevices = this.deviceRegistry.getAllOutputDevices(); + logger.info(`Sending discovery for ${allOutputDevices.length} Plejd output devices`); + allOutputDevices.forEach((outputDevice) => { + logger.debug(`Sending discovery for ${outputDevice.name}`); - this.deviceRegistry.allDevices.forEach((device) => { - logger.debug(`Sending discovery for ${device.name}`); - - const payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device); + const configPayload = getLightDiscoveryPayload(outputDevice); logger.info( - `Discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`, + `Discovered ${outputDevice.typeName} (${outputDevice.type}) named ${outputDevice.name} (${outputDevice.bleOutputAddress} : ${outputDevice.uniqueId}).`, ); - this.client.publish(getConfigPath(device), JSON.stringify(payload)); + this.client.publish(getTopicName(outputDevice, 'config'), JSON.stringify(configPayload)); setTimeout(() => { - this.client.publish(getAvailabilityTopic(device), 'online'); + this.client.publish(getTopicName(outputDevice, 'availability'), AVAILABLILITY.ONLINE); + }, 2000); + }); + + const allSceneDevices = this.deviceRegistry.getAllSceneDevices(); + logger.info(`Sending discovery for ${allSceneDevices.length} Plejd scene devices`); + allSceneDevices.forEach((sceneDevice) => { + logger.debug(`Sending discovery for ${sceneDevice.name}`); + + const configPayload = getScenehDiscoveryPayload(sceneDevice); + logger.info( + `Discovered ${sceneDevice.typeName} (${sceneDevice.type}) named ${sceneDevice.name} (${sceneDevice.bleOutputAddress} : ${sceneDevice.uniqueId}).`, + ); + + this.client.publish(getTopicName(sceneDevice, 'config'), JSON.stringify(configPayload)); + setTimeout(() => { + this.client.publish(getTopicName(sceneDevice, 'availability'), AVAILABLILITY.ONLINE); }, 2000); }); } - updateState(deviceId, data) { - const device = this.deviceRegistry.getDevice(deviceId); + /** + * @param {string} uniqueOutputId + * @param {{ state: boolean; brightness?: number; }} data + */ + updateOutputState(uniqueOutputId, data) { + const device = this.deviceRegistry.getOutputDevice(uniqueOutputId); if (!device) { - logger.warn(`Unknown device id ${deviceId} - not handled by us.`); + logger.warn(`Unknown output id ${uniqueOutputId} - not handled by us.`); return; } @@ -235,26 +280,29 @@ class MqttClient extends EventEmitter { let payload = null; if (device.type === 'switch') { - payload = data.state === 1 ? 'ON' : 'OFF'; + payload = getMqttStateString(data.state); } else { if (device.dimmable) { payload = { - state: data.state === 1 ? 'ON' : 'OFF', + state: getMqttStateString(data.state), brightness: data.brightness, }; } else { payload = { - state: data.state === 1 ? 'ON' : 'OFF', + state: getMqttStateString(data.state), }; } payload = JSON.stringify(payload); } - this.client.publish(getStateTopic(device), payload); - this.client.publish(getAvailabilityTopic(device), 'online'); + this.client.publish(getTopicName(device, 'state'), payload); + this.client.publish(getTopicName(device, 'availability'), AVAILABLILITY.ONLINE); } + /** + * @param {string} sceneId + */ sceneTriggered(sceneId) { logger.verbose(`Scene triggered: ${sceneId}`); this.client.publish(getSceneEventTopic(), JSON.stringify({ scene: sceneId })); From dbc0e02f11246c89de5181524f6424410edc1c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Wed, 31 Mar 2021 23:28:25 +0200 Subject: [PATCH 06/40] Minor fixes --- plejd/MqttClient.js | 6 ++---- plejd/PlejdApi.js | 21 ++++++++++----------- plejd/PlejdBLEHandler.js | 17 ++++++++++++----- plejd/PlejdDeviceCommunication.js | 11 ++++++----- 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index af79c03..5ad8a1d 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -13,10 +13,8 @@ const logger = Logger.getLogger('plejd-mqtt'); const discoveryPrefix = 'homeassistant'; const nodeId = 'plejd'; -const getMqttUniqueId = (/** @type {string} */ uniqueId) => `${nodeId}.${uniqueId}`; - const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`; -const getBaseTopic = (/** @type {{ uniqueId: string; type: string; }} */ plug) => `${discoveryPrefix}/${plug.type}/${nodeId}/${getMqttUniqueId(plug.uniqueId)}`; +const getBaseTopic = (/** @type {{ uniqueId: string; type: string; }} */ plug) => `${discoveryPrefix}/${plug.type}/${nodeId}/${plug.uniqueId}`; const getTopicName = ( /** @type {{ uniqueId: string; type: string; }} */ plug, @@ -48,7 +46,7 @@ const getLightDiscoveryPayload = ( ) => ({ schema: 'json', name: device.name, - unique_id: getMqttUniqueId(device.uniqueId), + unique_id: device.uniqueId, '~': getBaseTopic(device), state_topic: `~/${TOPICS.STATE}`, command_topic: `~/${TOPICS.COMMAND}`, diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index 01616c0..91d078a 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -320,6 +320,16 @@ class PlejdApi { (x) => x.deviceParseId === device.objectId, ); + if (!outputSettings) { + logger.verbose( + `No outputSettings found for ${device.title} (${device.deviceId}), assuming output 0`, + ); + } + + const bleOutputAddress = this.siteDetails.outputAddress[device.deviceId][ + outputSettings ? outputSettings.output : 0 + ]; + if (device.traits === TRAITS.NO_LOAD) { logger.warn( `Device ${device.title} (${device.deviceId}) has no load configured and will be excluded`, @@ -330,10 +340,6 @@ class PlejdApi { outputSettings.output, ); - const bleOutputAddress = this.siteDetails.outputAddress[device.deviceId][ - outputSettings.output - ]; - const plejdDevice = this.siteDetails.plejdDevices.find( (x) => x.deviceId === device.deviceId, ); @@ -361,13 +367,6 @@ class PlejdApi { }; this.deviceRegistry.addOutputDevice(outputDevice); - } else { - logger.warn( - `No outputSettings found for ${device.title} (${device.deviceId}), device will not be included`, - ); - logger.verbose( - 'Fallback cound potentially be implemented by assuming default deviceSettings[deviceId]', - ); } // What should we do with inputs?! diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 2bb062b..1ea20ad 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -154,21 +154,26 @@ class PlejBLEHandler extends EventEmitter { logger.info('BLE init done, waiting for devices.'); } - async sendCommand(command, uniqueOutputId, data) { + /** + * @param {string} command + * @param {number} bleOutputAddress + * @param {number} data + */ + async sendCommand(command, bleOutputAddress, data) { let payload; let brightnessVal; switch (command) { case COMMANDS.TURN_ON: - payload = this._createHexPayload(uniqueOutputId, BLE_CMD_STATE_CHANGE, '01'); + payload = this._createHexPayload(bleOutputAddress, BLE_CMD_STATE_CHANGE, '01'); break; case COMMANDS.TURN_OFF: - payload = this._createHexPayload(uniqueOutputId, BLE_CMD_STATE_CHANGE, '00'); + payload = this._createHexPayload(bleOutputAddress, BLE_CMD_STATE_CHANGE, '00'); break; case COMMANDS.DIM: // eslint-disable-next-line no-bitwise brightnessVal = (data << 8) | data; payload = this._createHexPayload( - uniqueOutputId, + bleOutputAddress, BLE_CMD_DIM2_CHANGE, `01${brightnessVal.toString(16).padStart(4, '0')}`, ); @@ -820,7 +825,9 @@ class PlejBLEHandler extends EventEmitter { // decoded.toString() could potentially be expensive logger.verbose(`Raw event received: ${decoded.toString('hex')}`); logger.verbose( - `Decoded: Device ${outputUniqueId}, cmd ${cmd.toString(16)}, state ${state}, dim ${dim}`, + `Decoded: Device ${outputUniqueId} (BLE address ${bleOutputAddress}), cmd ${cmd.toString( + 16, + )}, state ${state}, dim ${dim}`, ); } diff --git a/plejd/PlejdDeviceCommunication.js b/plejd/PlejdDeviceCommunication.js index 3c4431b..4321f94 100644 --- a/plejd/PlejdDeviceCommunication.js +++ b/plejd/PlejdDeviceCommunication.js @@ -255,9 +255,10 @@ class PlejdDeviceCommunication extends EventEmitter { return; } const queueItem = this.writeQueue.pop(); - const deviceName = this.deviceRegistry.getOutputDeviceName(queueItem.uniqueOutputId); + const device = this.deviceRegistry.getOutputDevice(queueItem.uniqueOutputId); + logger.debug( - `Write queue: Processing ${deviceName} (${queueItem.uniqueOutputId}). Command ${ + `Write queue: Processing ${device.name} (${queueItem.uniqueOutputId}). Command ${ queueItem.command }${queueItem.data ? ` ${queueItem.data}` : ''}. Total queue length: ${ this.writeQueue.length @@ -266,7 +267,7 @@ class PlejdDeviceCommunication extends EventEmitter { if (this.writeQueue.some((item) => item.uniqueOutputId === queueItem.uniqueOutputId)) { logger.verbose( - `Skipping ${deviceName} (${queueItem.uniqueOutputId}) ` + `Skipping ${device.name} (${queueItem.uniqueOutputId}) ` + `${queueItem.command} due to more recent command in queue.`, ); // Skip commands if new ones exist for the same uniqueOutputId @@ -276,7 +277,7 @@ class PlejdDeviceCommunication extends EventEmitter { try { await this.plejdBleHandler.sendCommand( queueItem.command, - queueItem.uniqueOutputId, + device.bleOutputAddress, queueItem.data, ); } catch (err) { @@ -287,7 +288,7 @@ class PlejdDeviceCommunication extends EventEmitter { this.writeQueue.push(queueItem); // Add back to top of queue to be processed next; } else { logger.error( - `Write queue: Exceeed max retry count (${MAX_RETRY_COUNT}) for ${deviceName} (${queueItem.uniqueOutputId}). Command ${queueItem.command} failed.`, + `Write queue: Exceeed max retry count (${MAX_RETRY_COUNT}) for ${device.name} (${queueItem.uniqueOutputId}). Command ${queueItem.command} failed.`, ); break; } From 754fe00c9a90ee9b351058afc7a07239a6d06d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Wed, 31 Mar 2021 23:37:12 +0200 Subject: [PATCH 07/40] Bump version to signify dev release and notify in changelog of breaking changes --- plejd/CHANGELOG.md | 15 +++++++++++++++ plejd/config.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/plejd/CHANGELOG.md b/plejd/CHANGELOG.md index ea70aee..43264bc 100644 --- a/plejd/CHANGELOG.md +++ b/plejd/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog hassio-plejd Home Assistant Plejd addon +## 0.8.0-dev + +**BREAKING - READ BELOW FIRST** + +Release 0.8 will break ALL EXISTING DEVICES. Unique mqtt id:s will change, meaning HA will create new devices. Scenes will be added as scenes not as switches. + +Recommendations to minimize impact + +- Optionally install MQTT explorer to bulk-delete discovered devices. If so - start MQTT explorer, connect, restart Plejd addon and then delete from MQTT explorer +- Shut down Plejd addon, disable autostart +- Reboot HA +- Go to Configuration => Integration => MQTT. Go to entities and after that devices and remove all Plejd devices (should be listed as unavailable) +- Upgrade addon to latest version and start +- All devices should now be back. With luck they will have the same HA id:s as before so most things should work. Room assignments, icons, etc will be gone though. + ## [0.7.1](https://github.com/icanos/hassio-plejd/tree/0.7.1) (2021-03-25) [Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.7.0...0.7.1) diff --git a/plejd/config.json b/plejd/config.json index 361eda4..3b0e94f 100644 --- a/plejd/config.json +++ b/plejd/config.json @@ -1,6 +1,6 @@ { "name": "Plejd", - "version": "0.7.1", + "version": "0.8.0-dev", "slug": "plejd", "description": "Adds support for the Swedish home automation devices from Plejd.", "url": "https://github.com/icanos/hassio-plejd/", From 464c17d9203f90adafcb73c48e9888580cdffd76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Thu, 1 Apr 2021 13:19:02 +0200 Subject: [PATCH 08/40] Fixes to scene handling and device unique id --- plejd/DeviceRegistry.js | 45 +++++++++++++++++++++++++++++----------- plejd/MqttClient.js | 1 - plejd/PlejdApi.js | 14 ++++++------- plejd/PlejdBLEHandler.js | 13 ++++++++---- plejd/SceneManager.js | 15 ++++++++------ 5 files changed, 57 insertions(+), 31 deletions(-) diff --git a/plejd/DeviceRegistry.js b/plejd/DeviceRegistry.js index d683ac3..6f07155 100644 --- a/plejd/DeviceRegistry.js +++ b/plejd/DeviceRegistry.js @@ -7,10 +7,12 @@ class DeviceRegistry { /** @private @type {Object.} */ devices = {}; - /** @private */ + /** @private @type {Object.} */ outputDeviceUniqueIdsByRoomId = {}; - /** @private */ + /** @private @type {Object.} */ outputUniqueIdByBleOutputAddress = {}; + /** @private @type {Object.} */ + sceneUniqueIdByBleOutputAddress = {}; /** @private @type {import('./types/ApiSite').ApiSite} */ apiSite; @@ -28,6 +30,13 @@ class DeviceRegistry { /** @param outputDevice {import('types/DeviceRegistry').OutputDevice} */ addOutputDevice(outputDevice) { + if (outputDevice.hiddenFromIntegrations || outputDevice.hiddenFromRoomList) { + logger.verbose(`Device ${outputDevice.name} is hidden and will not be included. + Hidden from room list: ${outputDevice.hiddenFromRoomList} + Hidden from integrations: ${outputDevice.hiddenFromIntegrations}`); + return; + } + this.outputDevices = { ...this.outputDevices, [outputDevice.uniqueId]: outputDevice, @@ -55,12 +64,6 @@ class DeviceRegistry { )}`, ); } - - if (outputDevice.hiddenFromIntegrations || outputDevice.hiddenFromRoomList) { - logger.verbose(`Device is hidden and should possibly not be included. - Hidden from room list: ${outputDevice.hiddenFromRoomList} - Hidden from integrations: ${outputDevice.hiddenFromIntegrations}`); - } } /** @param scene {import('types/DeviceRegistry').OutputDevice} */ @@ -85,6 +88,7 @@ class DeviceRegistry { clearSceneDevices() { this.sceneDevices = {}; + this.sceneUniqueIdByBleOutputAddress = {}; } /** @@ -135,12 +139,29 @@ class DeviceRegistry { return this.devices[deviceId]; } - getScene(sceneId) { - return this.sceneDevices[sceneId]; + /** + * @param {string} sceneUniqueId + */ + getScene(sceneUniqueId) { + return this.sceneDevices[sceneUniqueId]; } - getSceneName(sceneId) { - return (this.sceneDevices[sceneId] || {}).name; + /** + * @param {number} sceneBleAddress + */ + getSceneByBleAddress(sceneBleAddress) { + const sceneUniqueId = this.sceneUniqueIdByBleOutputAddress[sceneBleAddress]; + if (!sceneUniqueId) { + return null; + } + return this.sceneDevices[sceneUniqueId]; + } + + /** + * @param {string} sceneUniqueId + */ + getSceneName(sceneUniqueId) { + return (this.sceneDevices[sceneUniqueId] || {}).name; } // eslint-disable-next-line class-methods-use-this diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index 5ad8a1d..30b0f67 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -69,7 +69,6 @@ const getScenehDiscoveryPayload = ( ) => ({ name: sceneDevice.name, '~': getBaseTopic(sceneDevice), - state_topic: `~/${TOPICS.STATE}`, command_topic: `~/${TOPICS.COMMAND}`, optimistic: false, qos: 1, diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index 91d078a..2a3cc9b 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -325,19 +325,17 @@ class PlejdApi { `No outputSettings found for ${device.title} (${device.deviceId}), assuming output 0`, ); } - - const bleOutputAddress = this.siteDetails.outputAddress[device.deviceId][ - outputSettings ? outputSettings.output : 0 - ]; + const deviceOutput = outputSettings ? outputSettings.output : 0; + const bleOutputAddress = this.siteDetails.outputAddress[device.deviceId][deviceOutput]; if (device.traits === TRAITS.NO_LOAD) { logger.warn( `Device ${device.title} (${device.deviceId}) has no load configured and will be excluded`, ); - } else if (outputSettings) { + } else { const uniqueOutputId = this.deviceRegistry.getUniqueOutputId( device.deviceId, - outputSettings.output, + deviceOutput, ); const plejdDevice = this.siteDetails.plejdDevices.find( @@ -357,7 +355,7 @@ class PlejdApi { hiddenFromRoomList: device.hiddenFromRoomList, hiddenFromIntegrations: device.hiddenFromIntegrations, name: device.title, - output: outputSettings.output, + output: deviceOutput, roomId: device.roomId, state: undefined, type, @@ -448,7 +446,7 @@ class PlejdApi { output: undefined, roomId: undefined, state: false, - type: 'switch', + type: 'scene', typeName: 'Scene', version: undefined, uniqueId: scene.sceneId, diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 1ea20ad..638c94e 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -846,13 +846,18 @@ class PlejBLEHandler extends EventEmitter { command = state ? COMMANDS.TURN_ON : COMMANDS.TURN_OFF; this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data); } else if (cmd === BLE_CMD_SCENE_TRIG) { - const sceneId = state; - const sceneName = this.deviceRegistry.getSceneName(sceneId); + const sceneBleAddress = state; + const scene = this.deviceRegistry.getSceneByBleAddress(sceneBleAddress); - logger.debug(`${sceneName} (${sceneId}) scene triggered (device id ${outputUniqueId}).`); + if (!scene) { + logger.warn(`Scene with BLE address ${sceneBleAddress} could not be found, can't process message`); + return; + } + + logger.debug(`${scene.name} (${sceneBleAddress}) scene triggered (device id ${outputUniqueId}).`); command = COMMANDS.TRIGGER_SCENE; - data = { sceneId }; + data = { sceneId: scene.uniqueId }; this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data); } else if (cmd === BLE_CMD_TIME_UPDATE) { const now = new Date(); diff --git a/plejd/SceneManager.js b/plejd/SceneManager.js index 4f518ad..54dc256 100644 --- a/plejd/SceneManager.js +++ b/plejd/SceneManager.js @@ -7,7 +7,7 @@ class SceneManager { deviceRegistry; /** @private @type {import('./PlejdDeviceCommunication')} */ plejdDeviceCommunication; - /** @private @type {Object.} */ + /** @private @type {Object.} */ scenes; constructor(deviceRegistry, plejdDeviceCommunication) { @@ -23,15 +23,18 @@ class SceneManager { this.scenes = {}; scenes.forEach((scene) => { - const idx = this.deviceRegistry.getApiSite().sceneIndex[scene.sceneId]; - this.scenes[idx] = new Scene(this.deviceRegistry, idx, scene); + const sceneBleAddress = this.deviceRegistry.getApiSite().sceneIndex[scene.sceneId]; + this.scenes[scene.sceneId] = new Scene(this.deviceRegistry, sceneBleAddress, scene); }); } - executeScene(sceneId) { - const scene = this.scenes[sceneId]; + /** + * @param {string} sceneUniqueId + */ + executeScene(sceneUniqueId) { + const scene = this.scenes[sceneUniqueId]; if (!scene) { - logger.info(`Scene with id ${sceneId} not found`); + logger.info(`Scene with id ${sceneUniqueId} not found`); logger.verbose(`Scenes: ${JSON.stringify(this.scenes, null, 2)}`); return; } From 7c8373d2c751c7f28a69d98ff29d066757b1e588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Wed, 7 Apr 2021 10:26:33 +0200 Subject: [PATCH 09/40] Update mqtt package to v4 - Minor updates to other packages - Relates to #181 --- plejd/PlejdApi.js | 5 +---- plejd/PlejdBLEHandler.js | 8 ++++++-- plejd/package.json | 8 ++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index 2a3cc9b..fcc6029 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -333,10 +333,7 @@ class PlejdApi { `Device ${device.title} (${device.deviceId}) has no load configured and will be excluded`, ); } else { - const uniqueOutputId = this.deviceRegistry.getUniqueOutputId( - device.deviceId, - deviceOutput, - ); + const uniqueOutputId = this.deviceRegistry.getUniqueOutputId(device.deviceId, deviceOutput); const plejdDevice = this.siteDetails.plejdDevices.find( (x) => x.deviceId === device.deviceId, diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 638c94e..75eeaf0 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -850,11 +850,15 @@ class PlejBLEHandler extends EventEmitter { const scene = this.deviceRegistry.getSceneByBleAddress(sceneBleAddress); if (!scene) { - logger.warn(`Scene with BLE address ${sceneBleAddress} could not be found, can't process message`); + logger.warn( + `Scene with BLE address ${sceneBleAddress} could not be found, can't process message`, + ); return; } - logger.debug(`${scene.name} (${sceneBleAddress}) scene triggered (device id ${outputUniqueId}).`); + logger.debug( + `${scene.name} (${sceneBleAddress}) scene triggered (device id ${outputUniqueId}).`, + ); command = COMMANDS.TRIGGER_SCENE; data = { sceneId: scene.uniqueId }; diff --git a/plejd/package.json b/plejd/package.json index 6ba45e5..c2e504a 100644 --- a/plejd/package.json +++ b/plejd/package.json @@ -3,17 +3,17 @@ "@abandonware/bluetooth-hci-socket": "~0.5.3-7", "axios": "~0.21.1", "buffer-xor": "~2.0.2", - "dbus-next": "~0.9.1", + "dbus-next": "~0.9.2", "fs": "0.0.1-security", "jspack": "~0.0.4", - "mqtt": "~3.0.0", + "mqtt": "~4.2.6", "winston": "~3.3.3" }, "devDependencies": { "babel-eslint": "~10.1.0", - "eslint": "~7.18.0", + "eslint": "~7.23.0", "eslint-config-airbnb": "~18.2.1", - "eslint-config-prettier": "~7.2.0", + "eslint-config-prettier": "~8.1.0", "eslint-plugin-import": "~2.22.1", "eslint-plugin-prettier": "~3.3.1", "prettier": "~2.2.1" From c646bc55eb16475fcbdee528ba4b17be9fc0b93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Wed, 21 Apr 2021 21:07:17 +0200 Subject: [PATCH 10/40] Revert scenes to register as switches over mqtt --- plejd/MqttClient.js | 87 +++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 27 deletions(-) diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index 30b0f67..6f42903 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -13,21 +13,32 @@ const logger = Logger.getLogger('plejd-mqtt'); const discoveryPrefix = 'homeassistant'; const nodeId = 'plejd'; +const MQTT_TYPES = { + LIGHT: 'light', + SCENE: 'switch', // A bit problematic. Will assume scene if length === guid + SWITCH: 'switch', +}; + +const getMqttType = (/** @type {{ uniqueId: string; type: string; }} */ plug) => (plug.type === 'scene' ? MQTT_TYPES.SCENE : plug.type); + +const getBaseTopic = (/** @type {{ uniqueId: string; type: string; }} */ plug) => `${discoveryPrefix}/${getMqttType(plug)}/${nodeId}/${plug.uniqueId}`; +const getSceneEventTopic = () => 'plejd/event/scene'; const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`; -const getBaseTopic = (/** @type {{ uniqueId: string; type: string; }} */ plug) => `${discoveryPrefix}/${plug.type}/${nodeId}/${plug.uniqueId}`; const getTopicName = ( /** @type {{ uniqueId: string; type: string; }} */ plug, /** @type {'config' | 'state' | 'availability' | 'set'} */ topicType, ) => `${getBaseTopic(plug)}/${topicType}`; +// Very loosely check if string is a GUID/UUID +const isGuid = (s) => /^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/.test(s); + const TOPICS = { CONFIG: 'config', STATE: 'state', AVAILABILITY: 'availability', COMMAND: 'set', }; -const getSceneEventTopic = () => 'plejd/event/scene'; const decodeTopicRegexp = new RegExp( /(?[^[]+)\/(?.+)\/plejd\/(?.+)\/(?config|state|availability|set|scene)/, @@ -69,10 +80,12 @@ const getScenehDiscoveryPayload = ( ) => ({ name: sceneDevice.name, '~': getBaseTopic(sceneDevice), + state_topic: `~/${TOPICS.STATE}`, command_topic: `~/${TOPICS.COMMAND}`, + availability_topic: `~/${TOPICS.AVAILABILITY}`, optimistic: false, qos: 1, - retain: true, + retain: false, device: { identifiers: `${sceneDevice.uniqueId}`, manufacturer: 'Plejd', @@ -96,6 +109,9 @@ class MqttClient extends EventEmitter { stateChanged: 'stateChanged', }; + /** + * @param {import("DeviceRegistry")} deviceRegistry + */ constructor(deviceRegistry) { super(); @@ -107,8 +123,10 @@ class MqttClient extends EventEmitter { logger.info('Initializing MQTT connection for Plejd addon'); this.client = mqtt.connect(this.config.mqttBroker, { - username: this.config.mqttUsername, + clientId: `hassio-plejd_${Math.random().toString(16).substr(2, 8)}`, password: this.config.mqttPassword, + queueQoSZero: true, + username: this.config.mqttUsername, }); this.client.on('error', (err) => { @@ -144,28 +162,25 @@ class MqttClient extends EventEmitter { logger.info('Home Assistant has started. lets do discovery.'); this.emit(MqttClient.EVENTS.connected); } else { + logger.verbose(`Mqtt command ${topic}`); const decodedTopic = decodeTopic(topic); if (decodedTopic) { - let device = this.deviceRegistry.getOutputDevice(decodedTopic.id); + /** @type {import('types/DeviceRegistry').OutputDevice} */ + let device; + + if (decodedTopic.type === 'switch' && isGuid(decodedTopic.id)) { + // UUID device id => It's a scene + logger.verbose(`Getting scene ${decodedTopic.id} from registry`); + device = this.deviceRegistry.getScene(decodedTopic.id); + } else { + logger.verbose(`Getting device ${decodedTopic.id} from registry`); + device = this.deviceRegistry.getOutputDevice(decodedTopic.id); + } const messageString = message.toString(); const isJsonMessage = messageString.startsWith('{'); const command = isJsonMessage ? JSON.parse(messageString) : messageString; - if ( - !isJsonMessage - && messageString === 'ON' - && this.deviceRegistry.getScene(decodedTopic.id) - ) { - // Guess that id that got state command without dim value belongs to Scene, not Device - // This guess could very well be wrong depending on the installation... - logger.warn( - `Device id ${decodedTopic.id} belongs to both scene and device, guessing Scene is what should be set to ON. ` - + 'OFF commands still sent to device.', - ); - device = this.deviceRegistry.getScene(decodedTopic.id); - } - const deviceName = device ? device.name : ''; switch (decodedTopic.command) { @@ -218,7 +233,10 @@ class MqttClient extends EventEmitter { disconnect(callback) { this.deviceRegistry.getAllOutputDevices().forEach((outputDevice) => { - this.client.publish(getTopicName(outputDevice, 'availability'), AVAILABLILITY.OFFLINE); + this.client.publish(getTopicName(outputDevice, 'availability'), AVAILABLILITY.OFFLINE, { + retain: true, + qos: 1, + }); }); this.client.end(callback); } @@ -234,9 +252,15 @@ class MqttClient extends EventEmitter { `Discovered ${outputDevice.typeName} (${outputDevice.type}) named ${outputDevice.name} (${outputDevice.bleOutputAddress} : ${outputDevice.uniqueId}).`, ); - this.client.publish(getTopicName(outputDevice, 'config'), JSON.stringify(configPayload)); + this.client.publish(getTopicName(outputDevice, 'config'), JSON.stringify(configPayload), { + retain: true, + qos: 1, + }); setTimeout(() => { - this.client.publish(getTopicName(outputDevice, 'availability'), AVAILABLILITY.ONLINE); + this.client.publish(getTopicName(outputDevice, 'availability'), AVAILABLILITY.ONLINE, { + retain: true, + qos: 1, + }); }, 2000); }); @@ -250,9 +274,15 @@ class MqttClient extends EventEmitter { `Discovered ${sceneDevice.typeName} (${sceneDevice.type}) named ${sceneDevice.name} (${sceneDevice.bleOutputAddress} : ${sceneDevice.uniqueId}).`, ); - this.client.publish(getTopicName(sceneDevice, 'config'), JSON.stringify(configPayload)); + this.client.publish(getTopicName(sceneDevice, 'config'), JSON.stringify(configPayload), { + retain: true, + qos: 1, + }); setTimeout(() => { - this.client.publish(getTopicName(sceneDevice, 'availability'), AVAILABLILITY.ONLINE); + this.client.publish(getTopicName(sceneDevice, 'availability'), AVAILABLILITY.ONLINE, { + retain: true, + qos: 1, + }); }, 2000); }); } @@ -293,8 +323,11 @@ class MqttClient extends EventEmitter { payload = JSON.stringify(payload); } - this.client.publish(getTopicName(device, 'state'), payload); - this.client.publish(getTopicName(device, 'availability'), AVAILABLILITY.ONLINE); + this.client.publish(getTopicName(device, 'state'), payload, { retain: true, qos: 1 }); + this.client.publish(getTopicName(device, 'availability'), AVAILABLILITY.ONLINE, { + retain: true, + qos: 1, + }); } /** @@ -302,7 +335,7 @@ class MqttClient extends EventEmitter { */ sceneTriggered(sceneId) { logger.verbose(`Scene triggered: ${sceneId}`); - this.client.publish(getSceneEventTopic(), JSON.stringify({ scene: sceneId })); + this.client.publish(getSceneEventTopic(), JSON.stringify({ scene: sceneId }), { qos: 1 }); } } From aacf87a50cf7d44376271ae7bea76e6114e2d353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Fri, 23 Apr 2021 10:42:18 +0200 Subject: [PATCH 11/40] Revert mqtt v5 code since not supported by HassIO Mosquitto plugin --- plejd/MqttClient.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index 6f42903..cddfec9 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -125,6 +125,7 @@ class MqttClient extends EventEmitter { this.client = mqtt.connect(this.config.mqttBroker, { clientId: `hassio-plejd_${Math.random().toString(16).substr(2, 8)}`, password: this.config.mqttPassword, + protocolVersion: 4, // v5 not supported by HassIO Mosquitto queueQoSZero: true, username: this.config.mqttUsername, }); @@ -136,13 +137,22 @@ class MqttClient extends EventEmitter { this.client.on('connect', () => { logger.info('Connected to MQTT.'); - this.client.subscribe(startTopics, (err) => { - if (err) { - logger.error('Unable to subscribe to status topics', err); - } + this.client.subscribe( + startTopics, + // Add below when mqtt v5 is supported in Mosquitto 1.6 or 2.0 and forward + // { + // qos: 1, + // nl: true, // don't echo back messages sent + // rap: true, // retain as published - don't force retain = 0 + // }, + (err) => { + if (err) { + logger.error('Unable to subscribe to status topics', err); + } - this.emit(MqttClient.EVENTS.connected); - }); + this.emit(MqttClient.EVENTS.connected); + }, + ); this.client.subscribe(getSubscribePath(), (err) => { if (err) { @@ -162,7 +172,7 @@ class MqttClient extends EventEmitter { logger.info('Home Assistant has started. lets do discovery.'); this.emit(MqttClient.EVENTS.connected); } else { - logger.verbose(`Mqtt command ${topic}`); + logger.verbose(`Received mqtt message on ${topic}`); const decodedTopic = decodeTopic(topic); if (decodedTopic) { /** @type {import('types/DeviceRegistry').OutputDevice} */ From b10583336ee4947cdf6e4877580683110e0de164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Sat, 24 Apr 2021 09:22:36 +0200 Subject: [PATCH 12/40] Replace mqttt scene switches with actual scenes --- plejd/MqttClient.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index cddfec9..75e0b4a 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -15,7 +15,7 @@ const nodeId = 'plejd'; const MQTT_TYPES = { LIGHT: 'light', - SCENE: 'switch', // A bit problematic. Will assume scene if length === guid + SCENE: 'scene', // A bit problematic. Will assume scene if length === guid SWITCH: 'switch', }; @@ -79,20 +79,13 @@ const getScenehDiscoveryPayload = ( /** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice, ) => ({ name: sceneDevice.name, + unique_id: sceneDevice.uniqueId, '~': getBaseTopic(sceneDevice), - state_topic: `~/${TOPICS.STATE}`, command_topic: `~/${TOPICS.COMMAND}`, availability_topic: `~/${TOPICS.AVAILABILITY}`, - optimistic: false, + payload_on: 'ON', qos: 1, retain: false, - device: { - identifiers: `${sceneDevice.uniqueId}`, - manufacturer: 'Plejd', - model: sceneDevice.typeName, - name: sceneDevice.name, - sw_version: sceneDevice.version, - }, }); // #endregion @@ -178,7 +171,7 @@ class MqttClient extends EventEmitter { /** @type {import('types/DeviceRegistry').OutputDevice} */ let device; - if (decodedTopic.type === 'switch' && isGuid(decodedTopic.id)) { + if (decodedTopic.type === MQTT_TYPES.SCENE && isGuid(decodedTopic.id)) { // UUID device id => It's a scene logger.verbose(`Getting scene ${decodedTopic.id} from registry`); device = this.deviceRegistry.getScene(decodedTopic.id); From 74e381f0099241947e358bc97c6ebf4280a9e2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 26 Apr 2021 13:13:10 +0200 Subject: [PATCH 13/40] Device triggers for scenes implemented --- plejd/DeviceRegistry.js | 2 ++ plejd/MqttClient.js | 76 +++++++++++++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/plejd/DeviceRegistry.js b/plejd/DeviceRegistry.js index 6f07155..a031752 100644 --- a/plejd/DeviceRegistry.js +++ b/plejd/DeviceRegistry.js @@ -72,6 +72,8 @@ class DeviceRegistry { ...this.sceneDevices, [scene.uniqueId]: scene, }; + this.sceneUniqueIdByBleOutputAddress[scene.bleOutputAddress] = scene.uniqueId; + logger.verbose( `Added/updated scene: ${JSON.stringify(scene)}. ${ Object.keys(this.sceneDevices).length diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index 75e0b4a..92d7f4c 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -19,20 +19,6 @@ const MQTT_TYPES = { SWITCH: 'switch', }; -const getMqttType = (/** @type {{ uniqueId: string; type: string; }} */ plug) => (plug.type === 'scene' ? MQTT_TYPES.SCENE : plug.type); - -const getBaseTopic = (/** @type {{ uniqueId: string; type: string; }} */ plug) => `${discoveryPrefix}/${getMqttType(plug)}/${nodeId}/${plug.uniqueId}`; -const getSceneEventTopic = () => 'plejd/event/scene'; -const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`; - -const getTopicName = ( - /** @type {{ uniqueId: string; type: string; }} */ plug, - /** @type {'config' | 'state' | 'availability' | 'set'} */ topicType, -) => `${getBaseTopic(plug)}/${topicType}`; - -// Very loosely check if string is a GUID/UUID -const isGuid = (s) => /^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/.test(s); - const TOPICS = { CONFIG: 'config', STATE: 'state', @@ -40,6 +26,21 @@ const TOPICS = { COMMAND: 'set', }; +const getMqttType = (/** @type {{ uniqueId: string; type: string; }} */ plug) => (plug.type === 'scene' ? MQTT_TYPES.SCENE : plug.type); + +const getBaseTopic = (/** @type {{ uniqueId: string; type: string; }} */ plug) => `${discoveryPrefix}/${getMqttType(plug)}/${nodeId}/${plug.uniqueId}`; + +const getTopicName = ( + /** @type {{ uniqueId: string; type: string; }} */ plug, + /** @type {'config' | 'state' | 'availability' | 'set'} */ topicType, +) => `${getBaseTopic(plug)}/${topicType}`; + +const getSceneEventTopic = (sceneId) => `${getTopicName({ uniqueId: `${sceneId}_trigger`, type: 'device_automation' }, 'state')}`; +const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`; + +// Very loosely check if string is a GUID/UUID +const isGuid = (s) => /^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/.test(s); + const decodeTopicRegexp = new RegExp( /(?[^[]+)\/(?.+)\/plejd\/(?.+)\/(?config|state|availability|set|scene)/, ); @@ -75,7 +76,7 @@ const getLightDiscoveryPayload = ( }, }); -const getScenehDiscoveryPayload = ( +const getSceneDiscoveryPayload = ( /** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice, ) => ({ name: sceneDevice.name, @@ -88,6 +89,26 @@ const getScenehDiscoveryPayload = ( retain: false, }); +const getSceneDeviceTriggerhDiscoveryPayload = ( + /** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice, +) => ({ + automation_type: 'trigger', + '~': getBaseTopic({ + uniqueId: sceneDevice.uniqueId, + type: 'device_automation', + }), + qos: 1, + topic: `~/${TOPICS.STATE}`, + type: 'scene', + subtype: 'trigger', + device: { + identifiers: `${sceneDevice.uniqueId}`, + manufacturer: 'Plejd', + model: sceneDevice.typeName, + name: sceneDevice.name, + }, +}); + // #endregion const getMqttStateString = (/** @type {boolean} */ state) => (state ? 'ON' : 'OFF'); @@ -272,15 +293,34 @@ class MqttClient extends EventEmitter { allSceneDevices.forEach((sceneDevice) => { logger.debug(`Sending discovery for ${sceneDevice.name}`); - const configPayload = getScenehDiscoveryPayload(sceneDevice); + const sceneConfigPayload = getSceneDiscoveryPayload(sceneDevice); logger.info( `Discovered ${sceneDevice.typeName} (${sceneDevice.type}) named ${sceneDevice.name} (${sceneDevice.bleOutputAddress} : ${sceneDevice.uniqueId}).`, ); - this.client.publish(getTopicName(sceneDevice, 'config'), JSON.stringify(configPayload), { + this.client.publish(getTopicName(sceneDevice, 'config'), JSON.stringify(sceneConfigPayload), { retain: true, qos: 1, }); + + const sceneTriggerConfigPayload = getSceneDeviceTriggerhDiscoveryPayload(sceneDevice); + + this.client.publish( + getTopicName( + { + ...sceneDevice, + uniqueId: `${sceneDevice.uniqueId}_trigger`, + type: 'device_automation', + }, + 'config', + ), + JSON.stringify(sceneTriggerConfigPayload), + { + retain: true, + qos: 1, + }, + ); + setTimeout(() => { this.client.publish(getTopicName(sceneDevice, 'availability'), AVAILABLILITY.ONLINE, { retain: true, @@ -338,7 +378,7 @@ class MqttClient extends EventEmitter { */ sceneTriggered(sceneId) { logger.verbose(`Scene triggered: ${sceneId}`); - this.client.publish(getSceneEventTopic(), JSON.stringify({ scene: sceneId }), { qos: 1 }); + this.client.publish(getSceneEventTopic(sceneId), '', { qos: 1 }); } } From a633ffacb3f2f079e616db8224eab8fccb490a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Tue, 27 Apr 2021 11:36:38 +0200 Subject: [PATCH 14/40] Quick-fix to force switch devices to the light mqtt domain --- plejd/MqttClient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index 92d7f4c..c316798 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -26,7 +26,7 @@ const TOPICS = { COMMAND: 'set', }; -const getMqttType = (/** @type {{ uniqueId: string; type: string; }} */ plug) => (plug.type === 'scene' ? MQTT_TYPES.SCENE : plug.type); +const getMqttType = (/** @type {{ uniqueId: string; type: string; }} */ plug) => (plug.type === 'switch' ? MQTT_TYPES.LIGHT : plug.type); const getBaseTopic = (/** @type {{ uniqueId: string; type: string; }} */ plug) => `${discoveryPrefix}/${getMqttType(plug)}/${nodeId}/${plug.uniqueId}`; From 36e5c62b4f7d7dc3a145fb465c6fc38dbce7085a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Wed, 28 Apr 2021 20:07:53 +0200 Subject: [PATCH 15/40] Fix for devices without output --- plejd/PlejdApi.js | 67 ++++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index fcc6029..8609bbb 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -326,42 +326,49 @@ class PlejdApi { ); } const deviceOutput = outputSettings ? outputSettings.output : 0; - const bleOutputAddress = this.siteDetails.outputAddress[device.deviceId][deviceOutput]; + const outputAddress = this.siteDetails.outputAddress[device.deviceId]; - if (device.traits === TRAITS.NO_LOAD) { - logger.warn( - `Device ${device.title} (${device.deviceId}) has no load configured and will be excluded`, - ); - } else { - const uniqueOutputId = this.deviceRegistry.getUniqueOutputId(device.deviceId, deviceOutput); + if (outputAddress) { + const bleOutputAddress = outputAddress[deviceOutput]; - const plejdDevice = this.siteDetails.plejdDevices.find( - (x) => x.deviceId === device.deviceId, - ); + if (device.traits === TRAITS.NO_LOAD) { + logger.warn( + `Device ${device.title} (${device.deviceId}) has no load configured and will be excluded`, + ); + } else { + const uniqueOutputId = this.deviceRegistry.getUniqueOutputId( + device.deviceId, + deviceOutput, + ); - const dimmable = device.traits === TRAITS.DIMMABLE; - // dimmable = settings.dimCurve !== 'NonDimmable'; + const plejdDevice = this.siteDetails.plejdDevices.find( + (x) => x.deviceId === device.deviceId, + ); - const { name: typeName, type } = this._getDeviceType(plejdDevice); + const dimmable = device.traits === TRAITS.DIMMABLE; + // dimmable = settings.dimCurve !== 'NonDimmable'; - /** @type {import('types/DeviceRegistry').OutputDevice} */ - const outputDevice = { - bleOutputAddress, - deviceId: device.deviceId, - dimmable, - hiddenFromRoomList: device.hiddenFromRoomList, - hiddenFromIntegrations: device.hiddenFromIntegrations, - name: device.title, - output: deviceOutput, - roomId: device.roomId, - state: undefined, - type, - typeName, - version: plejdDevice.firmware.version, - uniqueId: uniqueOutputId, - }; + const { name: typeName, type } = this._getDeviceType(plejdDevice); - this.deviceRegistry.addOutputDevice(outputDevice); + /** @type {import('types/DeviceRegistry').OutputDevice} */ + const outputDevice = { + bleOutputAddress, + deviceId: device.deviceId, + dimmable, + hiddenFromRoomList: device.hiddenFromRoomList, + hiddenFromIntegrations: device.hiddenFromIntegrations, + name: device.title, + output: deviceOutput, + roomId: device.roomId, + state: undefined, + type, + typeName, + version: plejdDevice.firmware.version, + uniqueId: uniqueOutputId, + }; + + this.deviceRegistry.addOutputDevice(outputDevice); + } } // What should we do with inputs?! From 5a2129036453232f53f781045955ab970448b438 Mon Sep 17 00:00:00 2001 From: faanskit Date: Sat, 1 May 2021 19:41:29 +0200 Subject: [PATCH 16/40] Added support for WRT-10 as a Device Automation --- plejd/DeviceRegistry.js | 46 +++++++++++++++++++++++ plejd/MqttClient.js | 45 +++++++++++++++++++++++ plejd/PlejdAddon.js | 8 ++++ plejd/PlejdApi.js | 61 ++++++++++++++++++++----------- plejd/PlejdBLEHandler.js | 19 ++++++++++ plejd/PlejdDeviceCommunication.js | 2 + plejd/constants.js | 1 + plejd/types/DeviceRegistry.d.ts | 14 +++++++ 8 files changed, 174 insertions(+), 22 deletions(-) diff --git a/plejd/DeviceRegistry.js b/plejd/DeviceRegistry.js index a031752..bd2e0d8 100644 --- a/plejd/DeviceRegistry.js +++ b/plejd/DeviceRegistry.js @@ -22,12 +22,29 @@ class DeviceRegistry { outputDevices = {}; /** @private @type {import('types/DeviceRegistry').OutputDevices} */ sceneDevices = {}; + /** @private @type {import('types/DeviceRegistry').InputDevices} */ + inputDevices = {}; /** @param device {import('./types/ApiSite').Device} */ addPhysicalDevice(device) { this.devices[device.deviceId] = device; } + /** @param inputDevice {import('types/DeviceRegistry').InputDevice} */ + addInputDevice(inputDevice) { + this.inputDevices = { + ...this.inputDevices, + [inputDevice.uniqueId]: inputDevice, + }; + + logger.verbose( + `Added/updated input device: ${JSON.stringify(inputDevice)}. ${ + Object.keys(this.inputDevices).length + } output devices in total.`, + ); + this.outputUniqueIdByBleOutputAddress[`${inputDevice.bleOutputAddress}_${inputDevice.input}`] = inputDevice.uniqueId; + }; + /** @param outputDevice {import('types/DeviceRegistry').OutputDevice} */ addOutputDevice(outputDevice) { if (outputDevice.hiddenFromIntegrations || outputDevice.hiddenFromRoomList) { @@ -84,6 +101,7 @@ class DeviceRegistry { clearPlejdDevices() { this.devices = {}; this.outputDevices = {}; + this.inputDevices = {}; this.outputDeviceUniqueIdsByRoomId = {}; this.outputUniqueIdByBleOutputAddress = {}; } @@ -100,6 +118,13 @@ class DeviceRegistry { return Object.values(this.outputDevices); } + /** + * @returns {import('./types/DeviceRegistry').InputDevice[]} + */ + getAllInputDevices() { + return Object.values(this.inputDevices); + } + /** * @returns {import('./types/DeviceRegistry').OutputDevice[]} */ @@ -119,11 +144,23 @@ class DeviceRegistry { return this.outputDevices[uniqueOutputId]; } + /** + * @param {string} uniqueInputId + */ + getInputDevice(uniqueInputId) { + return this.inputDevices[uniqueInputId]; + } + /** @returns {import('./types/DeviceRegistry').OutputDevice} */ getOutputDeviceByBleOutputAddress(bleOutputAddress) { return this.outputDevices[this.outputUniqueIdByBleOutputAddress[bleOutputAddress]]; } + /** @returns {import('./types/DeviceRegistry').InputDevice} */ + getInputDeviceByBleOutputAddress(bleInputAddress, inputButton) { + return this.inputDevices[this.outputUniqueIdByBleOutputAddress[`${bleInputAddress}_${inputButton}`]]; + } + /** @returns {string[]} */ getOutputDeviceIdsByRoomId(roomId) { return this.outputDeviceUniqueIdsByRoomId[roomId]; @@ -133,6 +170,10 @@ class DeviceRegistry { return (this.outputDevices[uniqueOutputId] || {}).name; } + getInputDeviceName(uniqueInputId) { + return (this.inputDevices[uniqueInputId] || {}).name; + } + /** * @param {string } deviceId The physical device serial number * @return {import('./types/ApiSite').Device} @@ -171,6 +212,11 @@ class DeviceRegistry { return `${deviceId}_${outputIndex}`; } + // eslint-disable-next-line class-methods-use-this + getUniqueInputId(deviceId, inputIndex) { + return `${deviceId}_${inputIndex}`; + } + /** @param apiSite {import('./types/ApiSite').ApiSite} */ setApiSite(apiSite) { this.apiSite = apiSite; diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index c316798..4f1fb40 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -1,4 +1,5 @@ const EventEmitter = require('events'); +// @ts-ignore const mqtt = require('mqtt'); const Configuration = require('./Configuration'); @@ -35,6 +36,7 @@ const getTopicName = ( /** @type {'config' | 'state' | 'availability' | 'set'} */ topicType, ) => `${getBaseTopic(plug)}/${topicType}`; +const getButtonEventTopic = (deviceId) => `${getTopicName({ uniqueId: `${deviceId}`, type: 'device_automation' }, 'state')}`; const getSceneEventTopic = (sceneId) => `${getTopicName({ uniqueId: `${sceneId}_trigger`, type: 'device_automation' }, 'state')}`; const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`; @@ -89,6 +91,28 @@ const getSceneDiscoveryPayload = ( retain: false, }); +const getInputDeviceTriggerDiscoveryPayload = ( + /** @type {import('./types/DeviceRegistry').InputDevice} */ inputDevice, +) => ({ + automation_type: 'trigger', + payload: `${inputDevice.input}`, + '~': getBaseTopic({ + uniqueId: inputDevice.deviceId, + type: 'device_automation', + }), + qos: 1, + topic: `~/${TOPICS.STATE}`, + type: 'button_short_press', + subtype: `button_${inputDevice.input+1}`, + device: { + identifiers: `${inputDevice.deviceId}`, + manufacturer: 'Plejd', + model: inputDevice.typeName, + name: inputDevice.name, + }, +}); + + const getSceneDeviceTriggerhDiscoveryPayload = ( /** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice, ) => ({ @@ -288,6 +312,23 @@ class MqttClient extends EventEmitter { }, 2000); }); + const allInputDevices = this.deviceRegistry.getAllInputDevices(); + logger.info(`Sending discovery for ${allInputDevices.length} Plejd input devices`); + allInputDevices.forEach((inputDevice) => { + logger.debug(`Sending discovery for ${inputDevice.name}`); + const inputInputPayload = getInputDeviceTriggerDiscoveryPayload(inputDevice); + logger.info( + `Discovered ${inputDevice.typeName} (${inputDevice.type}) named ${inputDevice.name} (${inputDevice.bleOutputAddress} : ${inputDevice.uniqueId}).`, + ); + logger.verbose(`Publishing ${getTopicName(inputDevice, 'config')} with payload ${JSON.stringify(inputInputPayload)}`); + + this.client.publish(getTopicName(inputDevice, 'config'), JSON.stringify(inputInputPayload), { + retain: true, + qos: 1, + }); + + }) + const allSceneDevices = this.deviceRegistry.getAllSceneDevices(); logger.info(`Sending discovery for ${allSceneDevices.length} Plejd scene devices`); allSceneDevices.forEach((sceneDevice) => { @@ -373,6 +414,10 @@ class MqttClient extends EventEmitter { }); } + buttonPressed(data) { + logger.verbose(`Button ${data.deviceInput} pressed for deviceId ${data.deviceId}`); + this.client.publish(getButtonEventTopic(data.deviceId), `${data.deviceInput}`, { qos: 1 }); + } /** * @param {string} sceneId */ diff --git a/plejd/PlejdAddon.js b/plejd/PlejdAddon.js index cdb3d60..d78045b 100644 --- a/plejd/PlejdAddon.js +++ b/plejd/PlejdAddon.js @@ -126,6 +126,14 @@ class PlejdAddon extends EventEmitter { }, ); + this.plejdDeviceCommunication.on(PlejdDeviceCommunication.EVENTS.buttonPressed, (data) => { + try { + this.mqttClient.buttonPressed(data); + } catch (err) { + logger.error('Error in PlejdService.sceneTriggered callback', err); + } + }); + this.plejdDeviceCommunication.on(PlejdDeviceCommunication.EVENTS.sceneTriggered, (sceneId) => { try { this.mqttClient.sceneTriggered(sceneId); diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index 8609bbb..b6368c2 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -1,3 +1,4 @@ +// @ts-ignore const axios = require('axios').default; const fs = require('fs'); @@ -261,7 +262,7 @@ class PlejdApi { case 5: return { name: 'LED-10', type: 'light', dimmable: true }; case 6: - return { name: 'WPH-01', type: 'switch', dimmable: false }; + return { name: 'WPH-01', type: 'device_automation', dimmable: false }; case 7: return { name: 'REL-01', type: 'switch', dimmable: false }; case 8: @@ -369,30 +370,46 @@ class PlejdApi { this.deviceRegistry.addOutputDevice(outputDevice); } - } + } else { + // The device does not have an output. It can be assumed to be a WPH-01 or a WRT-01 + // Filter inputSettings for available buttons + const inputSettings = this.siteDetails.inputSettings.filter( + (x) => x.deviceId === device.deviceId && (x.buttonType == 'DirectionUp') || (x.buttonType == 'DirectionDown')); - // What should we do with inputs?! - // if (outputDevice.typeName === 'WPH-01') { - // // WPH-01 is special, it has two buttons which needs to be - // // registered separately. - // const inputs = this.siteDetails.inputAddress[deviceId]; - // const first = inputs[0]; - // const second = inputs[1]; + // For each found button, register the device as an inputDevice + inputSettings.forEach((input) => { - // this.deviceRegistry.addPlejdDevice({ - // ...outputDevice, - // id: first, - // name: `${device.title} left`, - // }); + const bleInputAddress = this.siteDetails.deviceAddress[input.deviceId]; + logger.verbose( + `Found input device (${input.deviceId}), with input ${input.input} having BLE address (${bleInputAddress})`, + ); - // this.deviceRegistry.addPlejdDevice({ - // ...outputDevice, - // id: second, - // name: `${device.title} right`, - // }); - // } else { - // this.deviceRegistry.addPlejdDevice(outputDevice); - // } + const plejdDevice = this.siteDetails.plejdDevices.find( + (x) => x.deviceId === device.deviceId, + ); + + const uniqueInputId = this.deviceRegistry.getUniqueInputId( + device.deviceId, + input.input, + ); + + const { name: typeName, type } = this._getDeviceType(plejdDevice); + + /** @type {import('types/DeviceRegistry').InputDevice} */ + const inputDevice = { + bleOutputAddress: bleInputAddress, + deviceId: device.deviceId, + name: device.title, + input: input.input, + roomId: device.roomId, + type, + typeName, + version: plejdDevice.firmware.version, + uniqueId: uniqueInputId, + }; + this.deviceRegistry.addInputDevice(inputDevice); + }); + }; }); } diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 75eeaf0..4d03865 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -1,3 +1,4 @@ +// @ts-ignore const dbus = require('dbus-next'); const crypto = require('crypto'); const xor = require('buffer-xor'); @@ -23,6 +24,7 @@ const BLE_CMD_DIM2_CHANGE = 0x0098; const BLE_CMD_STATE_CHANGE = 0x0097; const BLE_CMD_SCENE_TRIG = 0x0021; const BLE_CMD_TIME_UPDATE = 0x001b; +const BLE_CMD_REMOTE_CLICK = 0x0016; const BLE_BROADCAST_DEVICE_ID = 0x01; const BLE_REQUEST_NO_RESPONSE = 0x0110; @@ -906,6 +908,23 @@ class PlejBLEHandler extends EventEmitter { logger.info('Got time response. Plejd clock time in sync with Home Assistant time'); } } + } else if (cmd === BLE_CMD_REMOTE_CLICK) { + const inputBleAddress = state; + const inputButton = decoded.length > 7 ? decoded.readUInt8(6) : 0; + + const sourceDevice = this.deviceRegistry.getInputDeviceByBleOutputAddress(inputBleAddress, inputButton); + if (!sourceDevice) { + logger.warn( + `Scene with BLE address ${inputBleAddress} could not be found, can't process message`, + ); + return; + } + logger.verbose( + `WPH-10 button ${inputButton} at BLE address ${inputBleAddress} was pressed. Unique Id is ${sourceDevice.uniqueId}` + ); + command = COMMANDS.BUTTON_CLICK; + data = { deviceId: sourceDevice.deviceId, deviceInput: sourceDevice.input}; + this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data); } else { logger.verbose( `Command ${cmd.toString(16)} unknown. ${decoded.toString( diff --git a/plejd/PlejdDeviceCommunication.js b/plejd/PlejdDeviceCommunication.js index 4321f94..9c218a0 100644 --- a/plejd/PlejdDeviceCommunication.js +++ b/plejd/PlejdDeviceCommunication.js @@ -116,6 +116,8 @@ class PlejdDeviceCommunication extends EventEmitter { }); } else if (command === COMMANDS.TRIGGER_SCENE) { this.emit(PlejdDeviceCommunication.EVENTS.sceneTriggered, data.sceneId); + } else if (command === COMMANDS.BUTTON_CLICK) { + this.emit(PlejdDeviceCommunication.EVENTS.buttonPressed, data); } else { logger.warn(`Unknown ble command ${command}`); } diff --git a/plejd/constants.js b/plejd/constants.js index 0b69b3a..1b97f67 100644 --- a/plejd/constants.js +++ b/plejd/constants.js @@ -3,6 +3,7 @@ const COMMANDS = { TURN_OFF: 'Turn off', DIM: 'Dim', TRIGGER_SCENE: 'Trigger scene', + BUTTON_CLICK: 'Button click', }; module.exports = { COMMANDS }; diff --git a/plejd/types/DeviceRegistry.d.ts b/plejd/types/DeviceRegistry.d.ts index b93c509..4da82a4 100644 --- a/plejd/types/DeviceRegistry.d.ts +++ b/plejd/types/DeviceRegistry.d.ts @@ -19,3 +19,17 @@ export interface OutputDevice { version: string; uniqueId: string; } + +export type InputDevices = { [deviceIdAndOutput: string]: InputDevice }; + +export interface InputDevice { + bleOutputAddress: number; + deviceId: string; + name: string; + input: number; + roomId: string; + type: string; + typeName: string; + version: string; + uniqueId: string; +} From 236e533c8a36d341751ea6cf5ce6741640edc367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 3 May 2021 09:45:57 +0200 Subject: [PATCH 17/40] Set entiteis to correct type (switch/light) and fix availability for scenes - General cleanup and clarification of MQTT messages --- plejd/MqttClient.js | 150 +++++++++++++++++++++++++----------------- plejd/PlejdApi.js | 10 ++- plejd/types/Mqtt.d.ts | 25 +++++++ 3 files changed, 122 insertions(+), 63 deletions(-) create mode 100644 plejd/types/Mqtt.d.ts diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index c316798..1931e21 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -8,39 +8,37 @@ const startTopics = ['hass/status', 'homeassistant/status']; const logger = Logger.getLogger('plejd-mqtt'); -// #region discovery - const discoveryPrefix = 'homeassistant'; const nodeId = 'plejd'; +/** @type {import('./types/Mqtt').MQTT_TYPES} */ const MQTT_TYPES = { LIGHT: 'light', - SCENE: 'scene', // A bit problematic. Will assume scene if length === guid + SCENE: 'scene', SWITCH: 'switch', + DEVICE_AUTOMATION: 'device_automation', }; -const TOPICS = { +/** @type {import('./types/Mqtt').TOPIC_TYPES} */ +const TOPIC_TYPES = { CONFIG: 'config', STATE: 'state', AVAILABILITY: 'availability', COMMAND: 'set', }; -const getMqttType = (/** @type {{ uniqueId: string; type: string; }} */ plug) => (plug.type === 'switch' ? MQTT_TYPES.LIGHT : plug.type); - -const getBaseTopic = (/** @type {{ uniqueId: string; type: string; }} */ plug) => `${discoveryPrefix}/${getMqttType(plug)}/${nodeId}/${plug.uniqueId}`; +const getBaseTopic = (/** @type { string } */ uniqueId, /** @type { string } */ mqttDeviceType) => `${discoveryPrefix}/${mqttDeviceType}/${nodeId}/${uniqueId}`; const getTopicName = ( - /** @type {{ uniqueId: string; type: string; }} */ plug, - /** @type {'config' | 'state' | 'availability' | 'set'} */ topicType, -) => `${getBaseTopic(plug)}/${topicType}`; + /** @type { string } */ uniqueId, + /** @type { import('./types/Mqtt').MqttType } */ mqttDeviceType, + /** @type { import('./types/Mqtt').TopicType } */ topicType, +) => `${getBaseTopic(uniqueId, mqttDeviceType)}/${topicType}`; -const getSceneEventTopic = (sceneId) => `${getTopicName({ uniqueId: `${sceneId}_trigger`, type: 'device_automation' }, 'state')}`; +const getTriggerUniqueId = (/** @type { string } */ uniqueId) => `${uniqueId}_trigger`; +const getSceneEventTopic = (/** @type {string} */ sceneId) => `${getTopicName(getTriggerUniqueId(sceneId), MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`; const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`; -// Very loosely check if string is a GUID/UUID -const isGuid = (s) => /^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/.test(s); - const decodeTopicRegexp = new RegExp( /(?[^[]+)\/(?.+)\/plejd\/(?.+)\/(?config|state|availability|set|scene)/, ); @@ -53,20 +51,18 @@ const decodeTopic = (topic) => { return matches.groups; }; -const getLightDiscoveryPayload = ( +const getOutputDeviceDiscoveryPayload = ( /** @type {import('./types/DeviceRegistry').OutputDevice} */ device, ) => ({ - schema: 'json', name: device.name, unique_id: device.uniqueId, - '~': getBaseTopic(device), - state_topic: `~/${TOPICS.STATE}`, - command_topic: `~/${TOPICS.COMMAND}`, - availability_topic: `~/${TOPICS.AVAILABILITY}`, + '~': getBaseTopic(device.uniqueId, device.type), + state_topic: `~/${TOPIC_TYPES.STATE}`, + command_topic: `~/${TOPIC_TYPES.COMMAND}`, + availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`, optimistic: false, qos: 1, retain: true, - brightness: device.dimmable, device: { identifiers: `${device.deviceId}`, manufacturer: 'Plejd', @@ -74,6 +70,7 @@ const getLightDiscoveryPayload = ( name: device.name, sw_version: device.version, }, + ...(device.type === MQTT_TYPES.LIGHT ? { brightness: device.dimmable, schema: 'json' } : {}), }); const getSceneDiscoveryPayload = ( @@ -81,9 +78,9 @@ const getSceneDiscoveryPayload = ( ) => ({ name: sceneDevice.name, unique_id: sceneDevice.uniqueId, - '~': getBaseTopic(sceneDevice), - command_topic: `~/${TOPICS.COMMAND}`, - availability_topic: `~/${TOPICS.AVAILABILITY}`, + '~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.SCENE), + command_topic: `~/${TOPIC_TYPES.COMMAND}`, + availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`, payload_on: 'ON', qos: 1, retain: false, @@ -93,12 +90,9 @@ const getSceneDeviceTriggerhDiscoveryPayload = ( /** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice, ) => ({ automation_type: 'trigger', - '~': getBaseTopic({ - uniqueId: sceneDevice.uniqueId, - type: 'device_automation', - }), + '~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.DEVICE_AUTOMATION), qos: 1, - topic: `~/${TOPICS.STATE}`, + topic: `~/${TOPIC_TYPES.STATE}`, type: 'scene', subtype: 'trigger', device: { @@ -109,8 +103,6 @@ const getSceneDeviceTriggerhDiscoveryPayload = ( }, }); -// #endregion - const getMqttStateString = (/** @type {boolean} */ state) => (state ? 'ON' : 'OFF'); const AVAILABLILITY = { ONLINE: 'online', OFFLINE: 'offline' }; @@ -192,8 +184,7 @@ class MqttClient extends EventEmitter { /** @type {import('types/DeviceRegistry').OutputDevice} */ let device; - if (decodedTopic.type === MQTT_TYPES.SCENE && isGuid(decodedTopic.id)) { - // UUID device id => It's a scene + if (decodedTopic.type === MQTT_TYPES.SCENE) { logger.verbose(`Getting scene ${decodedTopic.id} from registry`); device = this.deviceRegistry.getScene(decodedTopic.id); } else { @@ -256,11 +247,29 @@ class MqttClient extends EventEmitter { } disconnect(callback) { + logger.info('Mqtt disconnect requested. Setting all devices as unavailable in HA...'); this.deviceRegistry.getAllOutputDevices().forEach((outputDevice) => { - this.client.publish(getTopicName(outputDevice, 'availability'), AVAILABLILITY.OFFLINE, { - retain: true, - qos: 1, - }); + const mqttType = outputDevice.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT; + this.client.publish( + getTopicName(outputDevice.uniqueId, mqttType, 'availability'), + AVAILABLILITY.OFFLINE, + { + retain: true, + qos: 1, + }, + ); + }); + + const allSceneDevices = this.deviceRegistry.getAllSceneDevices(); + allSceneDevices.forEach((sceneDevice) => { + this.client.publish( + getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY), + AVAILABLILITY.OFFLINE, + { + retain: true, + qos: 1, + }, + ); }); this.client.end(callback); } @@ -271,20 +280,29 @@ class MqttClient extends EventEmitter { allOutputDevices.forEach((outputDevice) => { logger.debug(`Sending discovery for ${outputDevice.name}`); - const configPayload = getLightDiscoveryPayload(outputDevice); + const configPayload = getOutputDeviceDiscoveryPayload(outputDevice); logger.info( `Discovered ${outputDevice.typeName} (${outputDevice.type}) named ${outputDevice.name} (${outputDevice.bleOutputAddress} : ${outputDevice.uniqueId}).`, ); - this.client.publish(getTopicName(outputDevice, 'config'), JSON.stringify(configPayload), { - retain: true, - qos: 1, - }); - setTimeout(() => { - this.client.publish(getTopicName(outputDevice, 'availability'), AVAILABLILITY.ONLINE, { + const mqttType = outputDevice.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT; + this.client.publish( + getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.CONFIG), + JSON.stringify(configPayload), + { retain: true, qos: 1, - }); + }, + ); + setTimeout(() => { + this.client.publish( + getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY), + AVAILABLILITY.ONLINE, + { + retain: true, + qos: 1, + }, + ); }, 2000); }); @@ -298,21 +316,22 @@ class MqttClient extends EventEmitter { `Discovered ${sceneDevice.typeName} (${sceneDevice.type}) named ${sceneDevice.name} (${sceneDevice.bleOutputAddress} : ${sceneDevice.uniqueId}).`, ); - this.client.publish(getTopicName(sceneDevice, 'config'), JSON.stringify(sceneConfigPayload), { - retain: true, - qos: 1, - }); + this.client.publish( + getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.CONFIG), + JSON.stringify(sceneConfigPayload), + { + retain: true, + qos: 1, + }, + ); const sceneTriggerConfigPayload = getSceneDeviceTriggerhDiscoveryPayload(sceneDevice); this.client.publish( getTopicName( - { - ...sceneDevice, - uniqueId: `${sceneDevice.uniqueId}_trigger`, - type: 'device_automation', - }, - 'config', + getTriggerUniqueId(sceneDevice.uniqueId), + MQTT_TYPES.DEVICE_AUTOMATION, + TOPIC_TYPES.CONFIG, ), JSON.stringify(sceneTriggerConfigPayload), { @@ -322,10 +341,14 @@ class MqttClient extends EventEmitter { ); setTimeout(() => { - this.client.publish(getTopicName(sceneDevice, 'availability'), AVAILABLILITY.ONLINE, { - retain: true, - qos: 1, - }); + this.client.publish( + getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY), + AVAILABLILITY.ONLINE, + { + retain: true, + qos: 1, + }, + ); }, 2000); }); } @@ -366,11 +389,16 @@ class MqttClient extends EventEmitter { payload = JSON.stringify(payload); } - this.client.publish(getTopicName(device, 'state'), payload, { retain: true, qos: 1 }); - this.client.publish(getTopicName(device, 'availability'), AVAILABLILITY.ONLINE, { + const mqttType = device.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT; + this.client.publish(getTopicName(device.uniqueId, mqttType, TOPIC_TYPES.STATE), payload, { retain: true, qos: 1, }); + // this.client.publish( + // getTopicName(device.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY), + // AVAILABLILITY.ONLINE, + // { retain: true, qos: 1 }, + // ); } /** diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index 8609bbb..b79b4fd 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -348,7 +348,13 @@ class PlejdApi { const dimmable = device.traits === TRAITS.DIMMABLE; // dimmable = settings.dimCurve !== 'NonDimmable'; - const { name: typeName, type } = this._getDeviceType(plejdDevice); + const { name: typeName, type: deviceType } = this._getDeviceType(plejdDevice); + let loadType = deviceType; + if (device.outputType === 'RELAY') { + loadType = 'switch'; + } else if (device.outputType === 'LIGHT') { + loadType = 'light'; + } /** @type {import('types/DeviceRegistry').OutputDevice} */ const outputDevice = { @@ -361,7 +367,7 @@ class PlejdApi { output: deviceOutput, roomId: device.roomId, state: undefined, - type, + type: loadType, typeName, version: plejdDevice.firmware.version, uniqueId: uniqueOutputId, diff --git a/plejd/types/Mqtt.d.ts b/plejd/types/Mqtt.d.ts new file mode 100644 index 0000000..3e3dc7a --- /dev/null +++ b/plejd/types/Mqtt.d.ts @@ -0,0 +1,25 @@ +/* eslint-disable no-use-before-define */ + +export type TopicType = 'config' | 'state' | 'availability' | 'set'; +export type TOPIC_TYPES = { [key: string]: TopicType }; + +export type MqttType = 'light' | 'scene' | 'switch' | 'device_automation'; +export type MQTT_TYPES = { [key: string]: MqttType }; + +export interface OutputDevice { + bleOutputAddress: number; + deviceId: string; + dim?: number; + dimmable: boolean; + hiddenFromRoomList?: boolean; + hiddenFromIntegrations?: boolean; + hiddenFromSceneList?: boolean; + name: string; + output: number; + roomId: string; + state: boolean | undefined; + type: string; + typeName: string; + version: string; + uniqueId: string; +} From cbcd33fde6dbef3204404497d3cba7df83d5d330 Mon Sep 17 00:00:00 2001 From: faanskit Date: Wed, 5 May 2021 12:33:30 +0200 Subject: [PATCH 18/40] Added support for WRT-01 --- plejd/PlejdApi.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index 2aab5f0..d129a29 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -270,7 +270,7 @@ class PlejdApi { // Unknown return { name: '-unknown-', type: 'light', dimmable: false }; case 10: - return { name: '-unknown-', type: 'light', dimmable: false }; + return { name: 'WRT-01', type: 'device_automation', dimmable: false }; case 12: // Unknown return { name: '-unknown-', type: 'light', dimmable: false }; @@ -380,7 +380,7 @@ class PlejdApi { // The device does not have an output. It can be assumed to be a WPH-01 or a WRT-01 // Filter inputSettings for available buttons const inputSettings = this.siteDetails.inputSettings.filter( - (x) => x.deviceId === device.deviceId && (x.buttonType == 'DirectionUp') || (x.buttonType == 'DirectionDown')); + (x) => x.deviceId === device.deviceId && (x.buttonType == 'DirectionUp') || (x.buttonType == 'DirectionDown') || (x.buttonType == 'RotateMesh')); // For each found button, register the device as an inputDevice inputSettings.forEach((input) => { From 78e1616b4ee274e8f13c327ecd82899d63b12415 Mon Sep 17 00:00:00 2001 From: faanskit Date: Wed, 5 May 2021 19:02:10 +0200 Subject: [PATCH 19/40] Updated to match the latest developer branch and modified according to feedback from review. --- plejd/DeviceRegistry.js | 4 ++-- plejd/MqttClient.js | 15 ++++++--------- plejd/PlejdApi.js | 5 ++--- plejd/PlejdBLEHandler.js | 3 +-- plejd/types/ApiSite.d.ts | 2 ++ plejd/types/DeviceRegistry.d.ts | 2 +- 6 files changed, 14 insertions(+), 17 deletions(-) diff --git a/plejd/DeviceRegistry.js b/plejd/DeviceRegistry.js index bd2e0d8..a5efba6 100644 --- a/plejd/DeviceRegistry.js +++ b/plejd/DeviceRegistry.js @@ -42,7 +42,7 @@ class DeviceRegistry { Object.keys(this.inputDevices).length } output devices in total.`, ); - this.outputUniqueIdByBleOutputAddress[`${inputDevice.bleOutputAddress}_${inputDevice.input}`] = inputDevice.uniqueId; + this.outputUniqueIdByBleOutputAddress[`${inputDevice.bleInputAddress}_${inputDevice.input}`] = inputDevice.uniqueId; }; /** @param outputDevice {import('types/DeviceRegistry').OutputDevice} */ @@ -157,7 +157,7 @@ class DeviceRegistry { } /** @returns {import('./types/DeviceRegistry').InputDevice} */ - getInputDeviceByBleOutputAddress(bleInputAddress, inputButton) { + getInputDeviceByBleInputAddress(bleInputAddress, inputButton) { return this.inputDevices[this.outputUniqueIdByBleOutputAddress[`${bleInputAddress}_${inputButton}`]]; } diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index 8d1b2e8..5c9df41 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -36,7 +36,7 @@ const getTopicName = ( /** @type { import('./types/Mqtt').TopicType } */ topicType, ) => `${getBaseTopic(uniqueId, mqttDeviceType)}/${topicType}`; -const getButtonEventTopic = (deviceId) => `${getTopicName({ uniqueId: `${deviceId}`, type: 'device_automation' }, 'state')}`; +const getButtonEventTopic = (/** @type {string} */ deviceId) => `${getTopicName(getTriggerUniqueId(deviceId), MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`; const getTriggerUniqueId = (/** @type { string } */ uniqueId) => `${uniqueId}_trigger`; const getSceneEventTopic = (/** @type {string} */ sceneId) => `${getTopicName(getTriggerUniqueId(sceneId), MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`; const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`; @@ -93,12 +93,9 @@ const getInputDeviceTriggerDiscoveryPayload = ( ) => ({ automation_type: 'trigger', payload: `${inputDevice.input}`, - '~': getBaseTopic({ - uniqueId: inputDevice.deviceId, - type: 'device_automation', - }), + '~': getBaseTopic(inputDevice.deviceId, MQTT_TYPES.DEVICE_AUTOMATION), qos: 1, - topic: `~/${TOPICS.STATE}`, + topic: `~/${TOPIC_TYPES.STATE}`, type: 'button_short_press', subtype: `button_${inputDevice.input+1}`, device: { @@ -336,11 +333,11 @@ class MqttClient extends EventEmitter { logger.debug(`Sending discovery for ${inputDevice.name}`); const inputInputPayload = getInputDeviceTriggerDiscoveryPayload(inputDevice); logger.info( - `Discovered ${inputDevice.typeName} (${inputDevice.type}) named ${inputDevice.name} (${inputDevice.bleOutputAddress} : ${inputDevice.uniqueId}).`, + `Discovered ${inputDevice.typeName} (${inputDevice.type}) named ${inputDevice.name} (${inputDevice.bleInputAddress} : ${inputDevice.uniqueId}).`, ); - logger.verbose(`Publishing ${getTopicName(inputDevice, 'config')} with payload ${JSON.stringify(inputInputPayload)}`); + logger.verbose(`Publishing ${getTopicName(inputDevice.uniqueId, MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.CONFIG)} with payload ${JSON.stringify(inputInputPayload)}`); - this.client.publish(getTopicName(inputDevice, 'config'), JSON.stringify(inputInputPayload), { + this.client.publish(getTopicName(inputDevice.uniqueId, MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.CONFIG), JSON.stringify(inputInputPayload), { retain: true, qos: 1, }); diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index d129a29..0b83b9b 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -1,4 +1,3 @@ -// @ts-ignore const axios = require('axios').default; const fs = require('fs'); @@ -380,7 +379,7 @@ class PlejdApi { // The device does not have an output. It can be assumed to be a WPH-01 or a WRT-01 // Filter inputSettings for available buttons const inputSettings = this.siteDetails.inputSettings.filter( - (x) => x.deviceId === device.deviceId && (x.buttonType == 'DirectionUp') || (x.buttonType == 'DirectionDown') || (x.buttonType == 'RotateMesh')); + (x) => x.deviceId === device.deviceId && ((x.buttonType == 'DirectionUp') || (x.buttonType == 'DirectionDown') || (x.buttonType == 'RotateMesh'))); // For each found button, register the device as an inputDevice inputSettings.forEach((input) => { @@ -403,7 +402,7 @@ class PlejdApi { /** @type {import('types/DeviceRegistry').InputDevice} */ const inputDevice = { - bleOutputAddress: bleInputAddress, + bleInputAddress: bleInputAddress, deviceId: device.deviceId, name: device.title, input: input.input, diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 4d03865..c81e2f9 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -1,4 +1,3 @@ -// @ts-ignore const dbus = require('dbus-next'); const crypto = require('crypto'); const xor = require('buffer-xor'); @@ -912,7 +911,7 @@ class PlejBLEHandler extends EventEmitter { const inputBleAddress = state; const inputButton = decoded.length > 7 ? decoded.readUInt8(6) : 0; - const sourceDevice = this.deviceRegistry.getInputDeviceByBleOutputAddress(inputBleAddress, inputButton); + const sourceDevice = this.deviceRegistry.getInputDeviceByBleInputAddress(inputBleAddress, inputButton); if (!sourceDevice) { logger.warn( `Scene with BLE address ${inputBleAddress} could not be found, can't process message`, diff --git a/plejd/types/ApiSite.d.ts b/plejd/types/ApiSite.d.ts index 856d056..4a6e562 100644 --- a/plejd/types/ApiSite.d.ts +++ b/plejd/types/ApiSite.d.ts @@ -280,6 +280,8 @@ export interface InputSetting { export enum ButtonType { PushButton = 'PushButton', + DirectionUp = 'DirectionUp', + DirectionDown = 'DirectionDown', RotateMesh = 'RotateMesh', Scene = 'Scene', } diff --git a/plejd/types/DeviceRegistry.d.ts b/plejd/types/DeviceRegistry.d.ts index 4da82a4..5ee68ae 100644 --- a/plejd/types/DeviceRegistry.d.ts +++ b/plejd/types/DeviceRegistry.d.ts @@ -23,7 +23,7 @@ export interface OutputDevice { export type InputDevices = { [deviceIdAndOutput: string]: InputDevice }; export interface InputDevice { - bleOutputAddress: number; + bleInputAddress: number; deviceId: string; name: string; input: number; From 5190341be6ec37c85e96b3cf05ebfe349ab0845d Mon Sep 17 00:00:00 2001 From: faanskit Date: Wed, 5 May 2021 19:05:52 +0200 Subject: [PATCH 20/40] removed ts-ignore --- plejd/MqttClient.js | 1 - 1 file changed, 1 deletion(-) diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index 5c9df41..aa803b6 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -1,5 +1,4 @@ const EventEmitter = require('events'); -// @ts-ignore const mqtt = require('mqtt'); const Configuration = require('./Configuration'); From 2610c1eaa4ff85b4dbaac85da650712d22fb3bb7 Mon Sep 17 00:00:00 2001 From: faanskit Date: Wed, 5 May 2021 19:18:43 +0200 Subject: [PATCH 21/40] Incorrect trigger --- plejd/MqttClient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index aa803b6..fb1d0ab 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -35,7 +35,7 @@ const getTopicName = ( /** @type { import('./types/Mqtt').TopicType } */ topicType, ) => `${getBaseTopic(uniqueId, mqttDeviceType)}/${topicType}`; -const getButtonEventTopic = (/** @type {string} */ deviceId) => `${getTopicName(getTriggerUniqueId(deviceId), MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`; +const getButtonEventTopic = (/** @type {string} */ deviceId) => `${getTopicName(deviceId, MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`; const getTriggerUniqueId = (/** @type { string } */ uniqueId) => `${uniqueId}_trigger`; const getSceneEventTopic = (/** @type {string} */ sceneId) => `${getTopicName(getTriggerUniqueId(sceneId), MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`; const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`; From 531aa8ee3e237631292f117eb7cc2245bf104dab Mon Sep 17 00:00:00 2001 From: faanskit Date: Wed, 5 May 2021 19:34:34 +0200 Subject: [PATCH 22/40] Fixed errors from npm run lint:fix --- plejd/DeviceRegistry.js | 10 ++++--- plejd/MqttClient.js | 29 +++++++++++------- plejd/PlejdApi.js | 64 ++++++++++++++++++++-------------------- plejd/PlejdBLEHandler.js | 9 ++++-- 4 files changed, 63 insertions(+), 49 deletions(-) diff --git a/plejd/DeviceRegistry.js b/plejd/DeviceRegistry.js index a5efba6..ebc66ec 100644 --- a/plejd/DeviceRegistry.js +++ b/plejd/DeviceRegistry.js @@ -43,7 +43,7 @@ class DeviceRegistry { } output devices in total.`, ); this.outputUniqueIdByBleOutputAddress[`${inputDevice.bleInputAddress}_${inputDevice.input}`] = inputDevice.uniqueId; - }; + } /** @param outputDevice {import('types/DeviceRegistry').OutputDevice} */ addOutputDevice(outputDevice) { @@ -121,7 +121,7 @@ class DeviceRegistry { /** * @returns {import('./types/DeviceRegistry').InputDevice[]} */ - getAllInputDevices() { + getAllInputDevices() { return Object.values(this.inputDevices); } @@ -147,7 +147,7 @@ class DeviceRegistry { /** * @param {string} uniqueInputId */ - getInputDevice(uniqueInputId) { + getInputDevice(uniqueInputId) { return this.inputDevices[uniqueInputId]; } @@ -158,7 +158,9 @@ class DeviceRegistry { /** @returns {import('./types/DeviceRegistry').InputDevice} */ getInputDeviceByBleInputAddress(bleInputAddress, inputButton) { - return this.inputDevices[this.outputUniqueIdByBleOutputAddress[`${bleInputAddress}_${inputButton}`]]; + return this.inputDevices[ + this.outputUniqueIdByBleOutputAddress[`${bleInputAddress}_${inputButton}`] + ]; } /** @returns {string[]} */ diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index fb1d0ab..c44f685 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -96,7 +96,7 @@ const getInputDeviceTriggerDiscoveryPayload = ( qos: 1, topic: `~/${TOPIC_TYPES.STATE}`, type: 'button_short_press', - subtype: `button_${inputDevice.input+1}`, + subtype: `button_${inputDevice.input + 1}`, device: { identifiers: `${inputDevice.deviceId}`, manufacturer: 'Plejd', @@ -105,7 +105,6 @@ const getInputDeviceTriggerDiscoveryPayload = ( }, }); - const getSceneDeviceTriggerhDiscoveryPayload = ( /** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice, ) => ({ @@ -334,14 +333,23 @@ class MqttClient extends EventEmitter { logger.info( `Discovered ${inputDevice.typeName} (${inputDevice.type}) named ${inputDevice.name} (${inputDevice.bleInputAddress} : ${inputDevice.uniqueId}).`, ); - logger.verbose(`Publishing ${getTopicName(inputDevice.uniqueId, MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.CONFIG)} with payload ${JSON.stringify(inputInputPayload)}`); + logger.verbose( + `Publishing ${getTopicName( + inputDevice.uniqueId, + MQTT_TYPES.DEVICE_AUTOMATION, + TOPIC_TYPES.CONFIG, + )} with payload ${JSON.stringify(inputInputPayload)}`, + ); - this.client.publish(getTopicName(inputDevice.uniqueId, MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.CONFIG), JSON.stringify(inputInputPayload), { - retain: true, - qos: 1, - }); - - }) + this.client.publish( + getTopicName(inputDevice.uniqueId, MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.CONFIG), + JSON.stringify(inputInputPayload), + { + retain: true, + qos: 1, + }, + ); + }); const allSceneDevices = this.deviceRegistry.getAllSceneDevices(); logger.info(`Sending discovery for ${allSceneDevices.length} Plejd scene devices`); @@ -438,10 +446,11 @@ class MqttClient extends EventEmitter { // ); } - buttonPressed(data) { + buttonPressed(data) { logger.verbose(`Button ${data.deviceInput} pressed for deviceId ${data.deviceId}`); this.client.publish(getButtonEventTopic(data.deviceId), `${data.deviceInput}`, { qos: 1 }); } + /** * @param {string} sceneId */ diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index 0b83b9b..db73ce1 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -379,42 +379,42 @@ class PlejdApi { // The device does not have an output. It can be assumed to be a WPH-01 or a WRT-01 // Filter inputSettings for available buttons const inputSettings = this.siteDetails.inputSettings.filter( - (x) => x.deviceId === device.deviceId && ((x.buttonType == 'DirectionUp') || (x.buttonType == 'DirectionDown') || (x.buttonType == 'RotateMesh'))); + (x) => x.deviceId === device.deviceId + && (x.buttonType === 'DirectionUp' + || x.buttonType === 'DirectionDown' + || x.buttonType === 'RotateMesh'), + ); - // For each found button, register the device as an inputDevice - inputSettings.forEach((input) => { + // For each found button, register the device as an inputDevice + inputSettings.forEach((input) => { + const bleInputAddress = this.siteDetails.deviceAddress[input.deviceId]; + logger.verbose( + `Found input device (${input.deviceId}), with input ${input.input} having BLE address (${bleInputAddress})`, + ); - const bleInputAddress = this.siteDetails.deviceAddress[input.deviceId]; - logger.verbose( - `Found input device (${input.deviceId}), with input ${input.input} having BLE address (${bleInputAddress})`, - ); + const plejdDevice = this.siteDetails.plejdDevices.find( + (x) => x.deviceId === device.deviceId, + ); - const plejdDevice = this.siteDetails.plejdDevices.find( - (x) => x.deviceId === device.deviceId, - ); + const uniqueInputId = this.deviceRegistry.getUniqueInputId(device.deviceId, input.input); - const uniqueInputId = this.deviceRegistry.getUniqueInputId( - device.deviceId, - input.input, - ); - - const { name: typeName, type } = this._getDeviceType(plejdDevice); - - /** @type {import('types/DeviceRegistry').InputDevice} */ - const inputDevice = { - bleInputAddress: bleInputAddress, - deviceId: device.deviceId, - name: device.title, - input: input.input, - roomId: device.roomId, - type, - typeName, - version: plejdDevice.firmware.version, - uniqueId: uniqueInputId, - }; - this.deviceRegistry.addInputDevice(inputDevice); - }); - }; + const { name: typeName, type } = this._getDeviceType(plejdDevice); + + /** @type {import('types/DeviceRegistry').InputDevice} */ + const inputDevice = { + bleInputAddress, + deviceId: device.deviceId, + name: device.title, + input: input.input, + roomId: device.roomId, + type, + typeName, + version: plejdDevice.firmware.version, + uniqueId: uniqueInputId, + }; + this.deviceRegistry.addInputDevice(inputDevice); + }); + } }); } diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index c81e2f9..7c0c0ad 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -911,7 +911,10 @@ class PlejBLEHandler extends EventEmitter { const inputBleAddress = state; const inputButton = decoded.length > 7 ? decoded.readUInt8(6) : 0; - const sourceDevice = this.deviceRegistry.getInputDeviceByBleInputAddress(inputBleAddress, inputButton); + const sourceDevice = this.deviceRegistry.getInputDeviceByBleInputAddress( + inputBleAddress, + inputButton, + ); if (!sourceDevice) { logger.warn( `Scene with BLE address ${inputBleAddress} could not be found, can't process message`, @@ -919,10 +922,10 @@ class PlejBLEHandler extends EventEmitter { return; } logger.verbose( - `WPH-10 button ${inputButton} at BLE address ${inputBleAddress} was pressed. Unique Id is ${sourceDevice.uniqueId}` + `WPH-10 button ${inputButton} at BLE address ${inputBleAddress} was pressed. Unique Id is ${sourceDevice.uniqueId}`, ); command = COMMANDS.BUTTON_CLICK; - data = { deviceId: sourceDevice.deviceId, deviceInput: sourceDevice.input}; + data = { deviceId: sourceDevice.deviceId, deviceInput: sourceDevice.input }; this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data); } else { logger.verbose( From c8890b8cd73c406c4d25d498d3ef775517c816d3 Mon Sep 17 00:00:00 2001 From: faanskit Date: Thu, 6 May 2021 07:58:06 +0200 Subject: [PATCH 23/40] Updates based om comments from @SweVictor --- plejd/DeviceRegistry.js | 13 ++++++++++--- plejd/MqttClient.js | 10 +++++++--- plejd/PlejdAddon.js | 17 ++++++++++------- plejd/PlejdBLEHandler.js | 2 +- plejd/PlejdDeviceCommunication.js | 2 +- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/plejd/DeviceRegistry.js b/plejd/DeviceRegistry.js index ebc66ec..2bfc63d 100644 --- a/plejd/DeviceRegistry.js +++ b/plejd/DeviceRegistry.js @@ -42,7 +42,9 @@ class DeviceRegistry { Object.keys(this.inputDevices).length } output devices in total.`, ); - this.outputUniqueIdByBleOutputAddress[`${inputDevice.bleInputAddress}_${inputDevice.input}`] = inputDevice.uniqueId; + this.outputUniqueIdByBleOutputAddress[ + this.getUniqueBLEId(inputDevice.bleInputAddress, inputDevice.input) + ] = inputDevice.uniqueId; } /** @param outputDevice {import('types/DeviceRegistry').OutputDevice} */ @@ -159,7 +161,7 @@ class DeviceRegistry { /** @returns {import('./types/DeviceRegistry').InputDevice} */ getInputDeviceByBleInputAddress(bleInputAddress, inputButton) { return this.inputDevices[ - this.outputUniqueIdByBleOutputAddress[`${bleInputAddress}_${inputButton}`] + this.outputUniqueIdByBleOutputAddress[this.getUniqueBLEId(bleInputAddress, inputButton)] ]; } @@ -216,7 +218,12 @@ class DeviceRegistry { // eslint-disable-next-line class-methods-use-this getUniqueInputId(deviceId, inputIndex) { - return `${deviceId}_${inputIndex}`; + return `${deviceId}_I_${inputIndex}`; + } + + // eslint-disable-next-line class-methods-use-this + getUniqueBLEId(bleAdress, inputIndex) { + return `${bleAdress}_${inputIndex}`; } /** @param apiSite {import('./types/ApiSite').ApiSite} */ diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index c44f685..64586bd 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -446,9 +446,13 @@ class MqttClient extends EventEmitter { // ); } - buttonPressed(data) { - logger.verbose(`Button ${data.deviceInput} pressed for deviceId ${data.deviceId}`); - this.client.publish(getButtonEventTopic(data.deviceId), `${data.deviceInput}`, { qos: 1 }); + /** + * @param {string} deviceId + * @param {string} deviceInput + */ + buttonPressed(deviceId, deviceInput) { + logger.verbose(`Button ${deviceInput} pressed for deviceId ${deviceId}`); + this.client.publish(getButtonEventTopic(deviceId), `${deviceInput}`, { qos: 1 }); } /** diff --git a/plejd/PlejdAddon.js b/plejd/PlejdAddon.js index d78045b..89e8310 100644 --- a/plejd/PlejdAddon.js +++ b/plejd/PlejdAddon.js @@ -126,13 +126,16 @@ class PlejdAddon extends EventEmitter { }, ); - this.plejdDeviceCommunication.on(PlejdDeviceCommunication.EVENTS.buttonPressed, (data) => { - try { - this.mqttClient.buttonPressed(data); - } catch (err) { - logger.error('Error in PlejdService.sceneTriggered callback', err); - } - }); + this.plejdDeviceCommunication.on( + PlejdDeviceCommunication.EVENTS.buttonPressed, + (deviceId, deviceInput) => { + try { + this.mqttClient.buttonPressed(deviceId, deviceInput); + } catch (err) { + logger.error('Error in PlejdService.buttonPressed callback', err); + } + }, + ); this.plejdDeviceCommunication.on(PlejdDeviceCommunication.EVENTS.sceneTriggered, (sceneId) => { try { diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 7c0c0ad..0a69961 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -922,7 +922,7 @@ class PlejBLEHandler extends EventEmitter { return; } logger.verbose( - `WPH-10 button ${inputButton} at BLE address ${inputBleAddress} was pressed. Unique Id is ${sourceDevice.uniqueId}`, + `A button (eg. WPH-01, WRT-01) ${inputButton} at BLE address ${inputBleAddress} was pressed. Unique Id is ${sourceDevice.uniqueId}`, ); command = COMMANDS.BUTTON_CLICK; data = { deviceId: sourceDevice.deviceId, deviceInput: sourceDevice.input }; diff --git a/plejd/PlejdDeviceCommunication.js b/plejd/PlejdDeviceCommunication.js index 9c218a0..8626b69 100644 --- a/plejd/PlejdDeviceCommunication.js +++ b/plejd/PlejdDeviceCommunication.js @@ -117,7 +117,7 @@ class PlejdDeviceCommunication extends EventEmitter { } else if (command === COMMANDS.TRIGGER_SCENE) { this.emit(PlejdDeviceCommunication.EVENTS.sceneTriggered, data.sceneId); } else if (command === COMMANDS.BUTTON_CLICK) { - this.emit(PlejdDeviceCommunication.EVENTS.buttonPressed, data); + this.emit(PlejdDeviceCommunication.EVENTS.buttonPressed, data.deviceId, data.deviceInput); } else { logger.warn(`Unknown ble command ${command}`); } From 535c8f6c0f2b9f85b43831f05e780e45e414f881 Mon Sep 17 00:00:00 2001 From: faanskit Date: Thu, 6 May 2021 13:28:13 +0200 Subject: [PATCH 24/40] Fixed the issue when a WPH were configured with single click to run a scene. Made code a bit more resilient. --- plejd/PlejdApi.js | 149 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 113 insertions(+), 36 deletions(-) diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index db73ce1..ab5cbf4 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -251,44 +251,124 @@ class PlejdApi { switch (parseInt(plejdDevice.hardwareId, 10)) { case 1: case 11: - return { name: 'DIM-01', type: 'light', dimmable: true }; + return { + name: 'DIM-01', + type: 'light', + dimmable: true, + broadcastClicks: false, + }; case 2: - return { name: 'DIM-02', type: 'light', dimmable: true }; + return { + name: 'DIM-02', + type: 'light', + dimmable: true, + broadcastClicks: false, + }; case 3: - return { name: 'CTR-01', type: 'light', dimmable: false }; + return { + name: 'CTR-01', + type: 'light', + dimmable: false, + broadcastClicks: false, + }; case 4: - return { name: 'GWY-01', type: 'sensor', dimmable: false }; + return { + name: 'GWY-01', + type: 'sensor', + dimmable: false, + broadcastClicks: false, + }; case 5: - return { name: 'LED-10', type: 'light', dimmable: true }; + return { + name: 'LED-10', + type: 'light', + dimmable: true, + broadcastClicks: false, + }; case 6: - return { name: 'WPH-01', type: 'device_automation', dimmable: false }; + return { + name: 'WPH-01', + type: 'device_automation', + dimmable: false, + broadcastClicks: true, + }; case 7: - return { name: 'REL-01', type: 'switch', dimmable: false }; + return { + name: 'REL-01', + type: 'switch', + dimmable: false, + broadcastClicks: false, + }; case 8: case 9: // Unknown - return { name: '-unknown-', type: 'light', dimmable: false }; + return { + name: '-unknown-', + type: 'light', + dimmable: false, + broadcastClicks: false, + }; case 10: - return { name: 'WRT-01', type: 'device_automation', dimmable: false }; + return { + name: 'WRT-01', + type: 'device_automation', + dimmable: false, + broadcastClicks: true, + }; case 12: // Unknown - return { name: '-unknown-', type: 'light', dimmable: false }; + return { + name: '-unknown-', + type: 'light', + dimmable: false, + broadcastClicks: false, + }; case 13: - return { name: 'Generic', type: 'light', dimmable: false }; + return { + name: 'Generic', + type: 'light', + dimmable: false, + broadcastClicks: false, + }; case 14: case 15: case 16: // Unknown - return { name: '-unknown-', type: 'light', dimmable: false }; + return { + name: '-unknown-', + type: 'light', + dimmable: false, + broadcastClicks: false, + }; case 17: - return { name: 'REL-01', type: 'switch', dimmable: false }; + return { + name: 'REL-01', + type: 'switch', + dimmable: false, + broadcastClicks: false, + }; case 18: - return { name: 'REL-02', type: 'switch', dimmable: false }; + return { + name: 'REL-02', + type: 'switch', + dimmable: false, + broadcastClicks: false, + }; case 19: // Unknown - return { name: '-unknown-', type: 'light', dimmable: false }; + return { + name: '-unknown-', + type: 'light', + dimmable: false, + broadcastClicks: false, + }; case 20: - return { name: 'SPR-01', type: 'switch', dimmable: false }; + return { + name: 'SPR-01', + type: 'switch', + dimmable: false, + broadcastClicks: false, + }; default: throw new Error(`Unknown device type with id ${plejdDevice.hardwareId}`); } @@ -379,10 +459,7 @@ class PlejdApi { // The device does not have an output. It can be assumed to be a WPH-01 or a WRT-01 // Filter inputSettings for available buttons const inputSettings = this.siteDetails.inputSettings.filter( - (x) => x.deviceId === device.deviceId - && (x.buttonType === 'DirectionUp' - || x.buttonType === 'DirectionDown' - || x.buttonType === 'RotateMesh'), + (x) => x.deviceId === device.deviceId, ); // For each found button, register the device as an inputDevice @@ -397,22 +474,22 @@ class PlejdApi { ); const uniqueInputId = this.deviceRegistry.getUniqueInputId(device.deviceId, input.input); - - const { name: typeName, type } = this._getDeviceType(plejdDevice); - - /** @type {import('types/DeviceRegistry').InputDevice} */ - const inputDevice = { - bleInputAddress, - deviceId: device.deviceId, - name: device.title, - input: input.input, - roomId: device.roomId, - type, - typeName, - version: plejdDevice.firmware.version, - uniqueId: uniqueInputId, - }; - this.deviceRegistry.addInputDevice(inputDevice); + const { name: typeName, type, broadcastClicks } = this._getDeviceType(plejdDevice); + if (broadcastClicks) { + /** @type {import('types/DeviceRegistry').InputDevice} */ + const inputDevice = { + bleInputAddress, + deviceId: device.deviceId, + name: device.title, + input: input.input, + roomId: device.roomId, + type, + typeName, + version: plejdDevice.firmware.version, + uniqueId: uniqueInputId, + }; + this.deviceRegistry.addInputDevice(inputDevice); + } }); } }); From 49024f539206b5ccb75471f10a875a11c724eb89 Mon Sep 17 00:00:00 2001 From: faanskit Date: Thu, 6 May 2021 13:43:51 +0200 Subject: [PATCH 25/40] Updated README.md to include information about WRT-01, tested by @vBrolin --- plejd/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plejd/README.md b/plejd/README.md index 336ea57..e126733 100644 --- a/plejd/README.md +++ b/plejd/README.md @@ -41,7 +41,8 @@ The add-on has been tested on the following platforms: - CTR-01 - REL-01 - REL-02 -- WPH-01 +- WPH-01 (Note: Available as Device Trigger short_button_press, button_1 .. button_4) +- WRT-01 (Note: Available as Device Trigger short_button_press, button_1. Rotation/dimming not offered by the device) ### Easy Installation From 7ba38d6acf306cb72dfca77e07ddca12e3afd376 Mon Sep 17 00:00:00 2001 From: faanskit Date: Fri, 7 May 2021 08:52:57 +0200 Subject: [PATCH 26/40] Lights and switches, including roomsAsLighs, is now proposing an area for Home Assistant based on the room name from Plejd. This by using suggested_area in MQTT config. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In many installations, Plejd units have the same name and are separated by the room they belong to. Eg. Badrum -> Spottar, Kök->Spottar, Sovrum->Taklampa, Kök->Taklampa With 8.0.0 devices can have the same name, and therefore it makes sense to now also support areas. --- plejd/MqttClient.js | 1 + plejd/PlejdApi.js | 5 +++++ plejd/types/DeviceRegistry.d.ts | 1 + 3 files changed, 7 insertions(+) diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index 64586bd..d10840b 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -69,6 +69,7 @@ const getOutputDeviceDiscoveryPayload = ( manufacturer: 'Plejd', model: device.typeName, name: device.name, + ...(device.roomName !== undefined ? { suggested_area: device.roomName } : {}), sw_version: device.version, }, ...(device.type === MQTT_TYPES.LIGHT ? { brightness: device.dimmable, schema: 'json' } : {}), diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index ab5cbf4..a338cba 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -436,6 +436,8 @@ class PlejdApi { loadType = 'light'; } + const room = this.siteDetails.rooms.find((x) => x.roomId === device.roomId); + /** @type {import('types/DeviceRegistry').OutputDevice} */ const outputDevice = { bleOutputAddress, @@ -446,6 +448,7 @@ class PlejdApi { name: device.title, output: deviceOutput, roomId: device.roomId, + roomName: room.title, state: undefined, type: loadType, typeName, @@ -519,6 +522,7 @@ class PlejdApi { name: room.title, output: undefined, roomId, + roomName: room.title, state: undefined, type: 'light', typeName: 'Room', @@ -548,6 +552,7 @@ class PlejdApi { name: scene.title, output: undefined, roomId: undefined, + roomName: undefined, state: false, type: 'scene', typeName: 'Scene', diff --git a/plejd/types/DeviceRegistry.d.ts b/plejd/types/DeviceRegistry.d.ts index 5ee68ae..9e4d6ba 100644 --- a/plejd/types/DeviceRegistry.d.ts +++ b/plejd/types/DeviceRegistry.d.ts @@ -13,6 +13,7 @@ export interface OutputDevice { name: string; output: number; roomId: string; + roomName: string; state: boolean | undefined; type: string; typeName: string; From dc3d2237da2fbd1a2ba427239f9496efc50faaa2 Mon Sep 17 00:00:00 2001 From: faanskit Date: Fri, 7 May 2021 10:59:52 +0200 Subject: [PATCH 27/40] Updating the Plejd / HA Device information model to be compatible with suggested_area. --- plejd/MqttClient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index d10840b..47c7d92 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -65,7 +65,7 @@ const getOutputDeviceDiscoveryPayload = ( qos: 1, retain: true, device: { - identifiers: `${device.deviceId}`, + identifiers: `${device.uniqueId}`, manufacturer: 'Plejd', model: device.typeName, name: device.name, From 672c957fd31f34ef46dfdfff3a2e6c47c145130b Mon Sep 17 00:00:00 2001 From: faanskit Date: Fri, 7 May 2021 18:25:47 +0200 Subject: [PATCH 28/40] Updated CHANGELOG to reflect additions with rooms. Updated README to provide clarity how different Plejd units will be propagated into Home Assistant. Updated README to solve #189 --- plejd/CHANGELOG.md | 4 ++-- plejd/README.md | 45 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/plejd/CHANGELOG.md b/plejd/CHANGELOG.md index 43264bc..6491c1c 100644 --- a/plejd/CHANGELOG.md +++ b/plejd/CHANGELOG.md @@ -4,7 +4,7 @@ **BREAKING - READ BELOW FIRST** -Release 0.8 will break ALL EXISTING DEVICES. Unique mqtt id:s will change, meaning HA will create new devices. Scenes will be added as scenes not as switches. +Release 0.8 will break ALL EXISTING DEVICES. Unique mqtt id:s will change, meaning HA will create new devices. Scenes will be added as scenes not as switches. Devices will be installed to Areas named by the rooms defined in the Plejd app (can be changed) Recommendations to minimize impact @@ -13,7 +13,7 @@ Recommendations to minimize impact - Reboot HA - Go to Configuration => Integration => MQTT. Go to entities and after that devices and remove all Plejd devices (should be listed as unavailable) - Upgrade addon to latest version and start -- All devices should now be back. With luck they will have the same HA id:s as before so most things should work. Room assignments, icons, etc will be gone though. +- All devices should now be back. With luck they will have the same HA id:s as before so most things should work. Room assignments, icons, automations, scenes, etc will have to be gone though. ## [0.7.1](https://github.com/icanos/hassio-plejd/tree/0.7.1) (2021-03-25) diff --git a/plejd/README.md b/plejd/README.md index e126733..a95baea 100644 --- a/plejd/README.md +++ b/plejd/README.md @@ -1,9 +1,32 @@ # Hass.io Plejd add-on Hass.io add-on for Plejd home automation devices. Gives you the ability to control the Plejd home automation devices through Home Assistant. -It uses MQTT to communicate with Home Assistant and supports auto discovery of the devices in range. +It uses MQTT to communicate with Home Assistant and supports auto discovery of the devices in range. Changed made in the Plejd app are propagated to Home Assistant. -It also supports notifications so that changed made in the Plejd app are propagated to Home Assistant. +Plejd output devices typically appears as either lights or switches in Home Assistant depending on how they are configured. + + + + + + + + + + + + + + + + + + + + + + +
DevicePlejd ConfigurationHome Assistant RoleComment
CTR-01Relay, LightLight
CTR-01Relay, OtherSwitch
REL-01Relay, LightLight
REL-01Relay, OtherSwitch
REL-02Relay, LightLight
REL-02Relay, OtherSwitch
SPR-01Relay, LightLightNot tested, not released by Plejd
SPR-01Relay, OtherSwitchNot tested, not released by Plejd
DIM-01-Light
DIM-02-Light
LED-10-Light
DAL-01--Not supported, not released by Plejd
WPH-01-Device Automationtype:button_short_press, subtype:button_1, button_2,button_3,button_4
WRT-01-Device Automationtype:button_short_press, subtype:button_1
GWY-01--
RTR-01--
Scene-Scene
Scene-Device Automationtype:scene, subtype:trigger
Room-AreaCan be changed by Home Assistant
Room-LightIf includeRoomsAsLights is set to true
Thanks to [ha-plejd](https://github.com/klali/ha-plejd) for inspiration. @@ -41,8 +64,8 @@ The add-on has been tested on the following platforms: - CTR-01 - REL-01 - REL-02 -- WPH-01 (Note: Available as Device Trigger short_button_press, button_1 .. button_4) -- WRT-01 (Note: Available as Device Trigger short_button_press, button_1. Rotation/dimming not offered by the device) +- WPH-01 +- WRT-01 ### Easy Installation @@ -52,6 +75,7 @@ Browse to your Home Assistant installation in a web browser and click on `Hass.i - Click on `Add-on Store` in the top navigation bar of that page. - Paste the URL to this repo https://github.com/icanos/hassio-plejd.git in the `Add new repository by URL` field and hit `Add`. - Scroll down and you should find a Plejd add-on that can be installed. Open that and install. +- Configure hassio-plejd (see below) - Enjoy! ### Manual Installation @@ -108,7 +132,16 @@ If you restore a backup from a 32bit system to a new 64bit system, use the Rebui ### Configuration -You need to add the MQTT integration to Home Assistant either by going to Configuration -> Integrations and clicking the Add Integration button, or by adding the following to your `configuration.yaml` file: +#### Simple MQTT Configurations +When you are using the official Mosquitto Broker from Home Assistant Add-on store, minimal configuration is required. +
+ + + +
mqttBrokermqtt://localhost
mqttUsernamehomeassistant
mqttPasswordMosquitto password, fetched via Configuration->Integrations->Mosquitto broker->Configure->RE-CONFIGURE->Password
+ +#### Advanced MQTT Configurations +For more advanced instllations, you need to add the MQTT integration to Home Assistant either by going to Configuration -> Integrations and clicking the Add Integration button, or by adding the following to your `configuration.yaml` file: ``` mqtt: @@ -121,6 +154,8 @@ mqtt: The above is used to notify the add-on when Home Assistant has started successfully and let the add-on send the discovery response (containing information about all Plejd devices found). +#### Configuration Parameters + The plugin needs you to configure some settings before working. You find these on the Add-on page after you've installed it. | Parameter | Value | From d239aacbc15c67219b3aba163eb02245c3babeb3 Mon Sep 17 00:00:00 2001 From: faanskit Date: Fri, 7 May 2021 18:37:07 +0200 Subject: [PATCH 29/40] Fixed error only showing in github for the readme --- plejd/README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/plejd/README.md b/plejd/README.md index a95baea..0763eb8 100644 --- a/plejd/README.md +++ b/plejd/README.md @@ -6,23 +6,23 @@ It uses MQTT to communicate with Home Assistant and supports auto discovery of t Plejd output devices typically appears as either lights or switches in Home Assistant depending on how they are configured. - - - - - - + + + + + + - - - + + + - - - + + + From 729559750e14b6f82a8edb3e5e0f6c6ccc237cf9 Mon Sep 17 00:00:00 2001 From: faanskit Date: Tue, 11 May 2021 21:03:48 +0200 Subject: [PATCH 30/40] Updated based on review feedback from @SweVictor. Thanks! --- plejd/PlejdApi.js | 3 +- plejd/README.md | 59 +++++++++++++++++---------------- plejd/types/DeviceRegistry.d.ts | 4 +-- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index a338cba..9208b95 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -437,6 +437,7 @@ class PlejdApi { } const room = this.siteDetails.rooms.find((x) => x.roomId === device.roomId); + const roomTitle = room ? room.title : undefined; /** @type {import('types/DeviceRegistry').OutputDevice} */ const outputDevice = { @@ -448,7 +449,7 @@ class PlejdApi { name: device.title, output: deviceOutput, roomId: device.roomId, - roomName: room.title, + roomName: roomTitle, state: undefined, type: loadType, typeName, diff --git a/plejd/README.md b/plejd/README.md index 0763eb8..1a7197e 100644 --- a/plejd/README.md +++ b/plejd/README.md @@ -4,29 +4,29 @@ Hass.io add-on for Plejd home automation devices. Gives you the ability to contr It uses MQTT to communicate with Home Assistant and supports auto discovery of the devices in range. Changed made in the Plejd app are propagated to Home Assistant. Plejd output devices typically appears as either lights or switches in Home Assistant depending on how they are configured. -
DevicePlejd ConfigurationHome Assistant RoleComment
CTR-01Relay, LightLight
CTR-01Relay, OtherSwitch
REL-01Relay, LightLight
REL-01Relay, OtherSwitch
REL-02Relay, LightLight
REL-02Relay, OtherSwitch
CTR-01Relay, LightLight
CTR-01Relay, OtherSwitch
REL-01Relay, LightLight
REL-01Relay, OtherSwitch
REL-02Relay, LightLight
REL-02Relay, OtherSwitch
SPR-01Relay, LightLightNot tested, not released by Plejd
SPR-01Relay, OtherSwitchNot tested, not released by Plejd
DIM-01-Light
DIM-02-Light
LED-10-Light
DIM-01-Light
DIM-02-Light
LED-10-Light
DAL-01--Not supported, not released by Plejd
WPH-01-Device Automationtype:button_short_press, subtype:button_1, button_2,button_3,button_4
WRT-01-Device Automationtype:button_short_press, subtype:button_1
GWY-01--
RTR-01--
Scene-Scene
GWY-01--
RTR-01--
Scene-Scene
Scene-Device Automationtype:scene, subtype:trigger
Room-AreaCan be changed by Home Assistant
Room-LightIf includeRoomsAsLights is set to true
- - - - - - - - - - - - - - - - - - - - - -
DevicePlejd ConfigurationHome Assistant RoleComment
CTR-01Relay, LightLight
CTR-01Relay, OtherSwitch
REL-01Relay, LightLight
REL-01Relay, OtherSwitch
REL-02Relay, LightLight
REL-02Relay, OtherSwitch
SPR-01Relay, LightLightNot tested, not released by Plejd
SPR-01Relay, OtherSwitchNot tested, not released by Plejd
DIM-01-Light
DIM-02-Light
LED-10-Light
DAL-01--Not supported, not released by Plejd
WPH-01-Device Automationtype:button_short_press, subtype:button_1, button_2,button_3,button_4
WRT-01-Device Automationtype:button_short_press, subtype:button_1
GWY-01--
RTR-01--
Scene-Scene
Scene-Device Automationtype:scene, subtype:trigger
Room-AreaCan be changed by Home Assistant
Room-LightIf includeRoomsAsLights is set to true
+ +| Device | Plejd Configuration | Home Assistant Role | Comment | +| ------ | ------------------- | ------------------- | --------------------------------------------------------------------- | +| CTR-01 | Relay, Light | Light | | +| CTR-01 | Relay, Other | Switch | | +| REL-01 | Relay, Light | Light | | +| REL-01 | Relay, Other | Switch | | +| REL-02 | Relay, Light | Light | | +| REL-02 | Relay, Other | Switch | | +| SPR-01 | Relay, Light | Light | Not tested, not supported | +| SPR-01 | Relay, Other | Switch | Not tested, not supported | +| DIM-01 | - | Light | | +| DIM-02 | - | Light | | +| LED-10 | - | Light | | +| DAL-01 | - | - | Not supported, not released by Plejd | +| WPH-01 | - | Device Automation | type:button_short_press, subtype:button_1, button_2,button_3,button_4 | +| WRT-01 | - | Device Automation | type:button_short_press, subtype:button_1 | +| GWY-01 | - | - | | +| RTR-01 | - | - | | +| Scene | - | Scene | | +| Scene | - | Device Automation | type:scene, subtype:trigger | +| Room | - | Area | Can be changed by Home Assistant | +| Room | - | Light | If includeRoomsAsLights is set to true | Thanks to [ha-plejd](https://github.com/klali/ha-plejd) for inspiration. @@ -133,14 +133,17 @@ If you restore a backup from a 32bit system to a new 64bit system, use the Rebui ### Configuration #### Simple MQTT Configurations + When you are using the official Mosquitto Broker from Home Assistant Add-on store, minimal configuration is required. -
- - - -
mqttBrokermqtt://localhost
mqttUsernamehomeassistant
mqttPasswordMosquitto password, fetched via Configuration->Integrations->Mosquitto broker->Configure->RE-CONFIGURE->Password
+ +| Parameter | Value | +| ------------ | ---------------------------------------------------------------------------------------------------------------- | +| mqttBroker | mqtt://localhost | +| mqttUsername | homeassistant | +| mqttPassword | Mosquitto password, fetched via Configuration->Integrations->Mosquitto broker->Configure->RE-CONFIGURE->Password | #### Advanced MQTT Configurations + For more advanced instllations, you need to add the MQTT integration to Home Assistant either by going to Configuration -> Integrations and clicking the Add Integration button, or by adding the following to your `configuration.yaml` file: ``` diff --git a/plejd/types/DeviceRegistry.d.ts b/plejd/types/DeviceRegistry.d.ts index 9e4d6ba..2d705cc 100644 --- a/plejd/types/DeviceRegistry.d.ts +++ b/plejd/types/DeviceRegistry.d.ts @@ -12,8 +12,8 @@ export interface OutputDevice { hiddenFromSceneList?: boolean; name: string; output: number; - roomId: string; - roomName: string; + roomId: string | undefined; + roomName: string | undefined; state: boolean | undefined; type: string; typeName: string; From de30a6aa3d7dd0c33c635337840fb469466f3167 Mon Sep 17 00:00:00 2001 From: faanskit Date: Wed, 12 May 2021 10:45:27 +0200 Subject: [PATCH 31/40] changed back behaviour of rooms as devices. Will not appear in suggested rooms --- plejd/PlejdApi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index 9208b95..45e9fb5 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -523,7 +523,7 @@ class PlejdApi { name: room.title, output: undefined, roomId, - roomName: room.title, + roomName: undefined, state: undefined, type: 'light', typeName: 'Room', From 78b815d3f04eef5a1e161af9c4641a98bf2ffd3a Mon Sep 17 00:00:00 2001 From: faanskit Date: Wed, 12 May 2021 11:31:43 +0200 Subject: [PATCH 32/40] Updated legacy based on feedback from @SweVictor --- plejd/PlejdApi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index 45e9fb5..3f4a00f 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -522,7 +522,7 @@ class PlejdApi { hiddenFromIntegrations: false, name: room.title, output: undefined, - roomId, + roomId: undefined, roomName: undefined, state: undefined, type: 'light', From 9cf3b3bdd650b9673af81b9462bd37f96df91df5 Mon Sep 17 00:00:00 2001 From: faanskit Date: Thu, 20 May 2021 07:24:22 +0200 Subject: [PATCH 33/40] Fix for issue discussed im #198. Scene device triggers were not unique from Scenes and device_trigger /config topic was too long --- plejd/MqttClient.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index 47c7d92..5be9434 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -36,7 +36,7 @@ const getTopicName = ( ) => `${getBaseTopic(uniqueId, mqttDeviceType)}/${topicType}`; const getButtonEventTopic = (/** @type {string} */ deviceId) => `${getTopicName(deviceId, MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`; -const getTriggerUniqueId = (/** @type { string } */ uniqueId) => `${uniqueId}_trigger`; +const getTriggerUniqueId = (/** @type { string } */ uniqueId) => `${uniqueId}_trig`; const getSceneEventTopic = (/** @type {string} */ sceneId) => `${getTopicName(getTriggerUniqueId(sceneId), MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`; const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`; @@ -110,13 +110,13 @@ const getSceneDeviceTriggerhDiscoveryPayload = ( /** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice, ) => ({ automation_type: 'trigger', - '~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.DEVICE_AUTOMATION), + '~': getBaseTopic(`${sceneDevice.uniqueId}_trig`, MQTT_TYPES.DEVICE_AUTOMATION), qos: 1, topic: `~/${TOPIC_TYPES.STATE}`, type: 'scene', subtype: 'trigger', device: { - identifiers: `${sceneDevice.uniqueId}`, + identifiers: `${sceneDevice.uniqueId}_trigger`, manufacturer: 'Plejd', model: sceneDevice.typeName, name: sceneDevice.name, From 93eca16639e6a6bd1098d061cf6cd8e2da46d568 Mon Sep 17 00:00:00 2001 From: faanskit Date: Thu, 20 May 2021 13:49:44 +0200 Subject: [PATCH 34/40] Added support to report back to HA via MQTT when a Scene is executed, so that following device_automations gets executed. This will make Plejd Scene execution behave the same from Plejd App, Plejd Hardware and Home Assistant Scene triggered - in the eyes of Home Assistant --- plejd/PlejdAddon.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plejd/PlejdAddon.js b/plejd/PlejdAddon.js index 89e8310..323678b 100644 --- a/plejd/PlejdAddon.js +++ b/plejd/PlejdAddon.js @@ -76,6 +76,15 @@ class PlejdAddon extends EventEmitter { // we're triggering a scene, lets do that and jump out. // since scenes aren't "real" devices. this.sceneManager.executeScene(uniqueId); + + // since the scene doesn't get any updates on whether it's executed or not, + // we fake this by directly send the sceneTriggered back to HA in order for + // it continue to acto on the scene (for non-plejd devices). + try { + this.mqttClient.sceneTriggered(uniqueId); + } catch (err) { + logger.error('Error in PlejdService.sceneTriggered callback', err); + } return; } From 67aa612f94f1c2159083f2e6068661826a9c32a5 Mon Sep 17 00:00:00 2001 From: kopo Date: Mon, 31 May 2021 14:38:12 +0200 Subject: [PATCH 35/40] Added more documentation to install steps --- Details.md | 28 ++++++++++++++++++++++++++++ plejd/README.md | 25 +++++++++++++++---------- 2 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 Details.md diff --git a/Details.md b/Details.md new file mode 100644 index 0000000..3aabbef --- /dev/null +++ b/Details.md @@ -0,0 +1,28 @@ +# Details for Home Assistant beginners +If you can reach your Home Assistant at [http://homeassistant.local:8123](http://homeassistant.local:8123) the links below should work. + +## Mosquitto +Head over to Supervisor -> Add-on Store and search for `mosquitto broker`. +Install it and then start [mosquito addon link](http://homeassistant.local:8123/hassio/addon/core_mosquitto/info) + + +## Add api user for Mosquito +Add a Home Assistant user for the Plejd addon to be able to connect to Mosquito [Configuration -> Users](http://homeassistant.local:8123/config/users) +Call the user e.g. `mqtt-api-user`, set a password and save + +## Plejd +Follow the `Easy Installation` in [README.MD](plejd/README.md) +And `Configuration Parameters` on the same page. +The only parameters needing a value are + * site + * username + * password + * mqttUsername e.g. `mqtt-api-user` + * mqttPassword + +Now you can start the Plejd add-on + +## Where are the lights? +Head over to [Configuration -> Integrations](http://homeassistant.local:8123/config/integrations) and click Configure on MQTT +After this step a new `Mosquito broker` should appear on the same page. If everything was setup correctly. It will list your lights under +`1 entity`/`n entities` \ No newline at end of file diff --git a/plejd/README.md b/plejd/README.md index 1a7197e..bf48f53 100644 --- a/plejd/README.md +++ b/plejd/README.md @@ -69,13 +69,14 @@ The add-on has been tested on the following platforms: ### Easy Installation -Browse to your Home Assistant installation in a web browser and click on `Hass.io` in the navigation bar to the left. +Browse to your Home Assistant installation in a web browser and click on `Supervisor` in the navigation bar to the left. -- Open the Home Assistant web console and click `Hass.io` in the menu on the left side. +- Open the Home Assistant web console and click `Supervisor` in the menu on the left side. - Click on `Add-on Store` in the top navigation bar of that page. -- Paste the URL to this repo https://github.com/icanos/hassio-plejd.git in the `Add new repository by URL` field and hit `Add`. +- Click on the three vertical dots to the far right and chose `Repositories` +- Paste the URL to this repo https://github.com/icanos/hassio-plejd.git in the `Add` field and hit `Add`. - Scroll down and you should find a Plejd add-on that can be installed. Open that and install. -- Configure hassio-plejd (see below) +- Configure hassio-plejd (see below). - Enjoy! ### Manual Installation @@ -85,12 +86,15 @@ Browse your Hass.io installation using a tool that allows you to manage files, f - Open the `/addon` directory - Create a new folder named `hassio-plejd` - Copy all files from this repository into that newly created one. -- Open the Home Assistant web console and click `Hass.io` in the menu on the left side. +- Open the Home Assistant web console and click `Supervisor` in the menu on the left side. - Click on `Add-on Store` in the top navigation bar of that page. - Click on the refresh button in the upper right corner. - A new Local Add-on should appear named Plejd. Open that and install. - Enjoy! +### Detailed Home Assistant instructions +[The details](../Details.md) + ### Install older versions or developemnt version To install older versions, follow the "Manual Installation" instructions above, but copy the code from [one of the releases](https://github.com/icanos/hassio-plejd/releases). To test new functionality you can download the development version, available in the [develop branch](https://github.com/icanos/hassio-plejd/tree/develop). @@ -134,13 +138,14 @@ If you restore a backup from a 32bit system to a new 64bit system, use the Rebui #### Simple MQTT Configurations -When you are using the official Mosquitto Broker from Home Assistant Add-on store, minimal configuration is required. +When you are using the official Mosquitto Broker from Home Assistant Add-on store, minimal configuration is required. +Create a user in [Configuration -> Users](http://homeassistant.local:8123/config/users) named e.g. mqtt-api-user | Parameter | Value | | ------------ | ---------------------------------------------------------------------------------------------------------------- | -| mqttBroker | mqtt://localhost | -| mqttUsername | homeassistant | -| mqttPassword | Mosquitto password, fetched via Configuration->Integrations->Mosquitto broker->Configure->RE-CONFIGURE->Password | +| mqttBroker | mqtt:// | +| mqttUsername | Arbitrary Home Assistant User e.g. mqtt-api-user | +| mqttPassword | Users password | #### Advanced MQTT Configurations @@ -166,7 +171,7 @@ The plugin needs you to configure some settings before working. You find these o | site | Name of your Plejd site, the name is displayed in the Plejd app (top bar). | | username | Username of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. | | password | Password of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. | -| mqttBroker | URL of the MQTT Broker, eg. mqtt://localhost | +| mqttBroker | URL of the MQTT Broker, eg. mqtt:// | | mqttUsername | Username of the MQTT broker | | mqttPassword | Password of the MQTT broker | | includeRoomsAsLights | Adds all rooms as lights, making it possible to turn on/off lights by room instead. Setting this to false will ignore all rooms. | From 71dbcc0b44721ed1593f21920080fa2b06967236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Wed, 9 Jun 2021 09:23:24 +0200 Subject: [PATCH 36/40] Move Details.md to /plejd folder --- Details.md => plejd/Details.md | 0 plejd/README.md | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename Details.md => plejd/Details.md (100%) diff --git a/Details.md b/plejd/Details.md similarity index 100% rename from Details.md rename to plejd/Details.md diff --git a/plejd/README.md b/plejd/README.md index bf48f53..4661580 100644 --- a/plejd/README.md +++ b/plejd/README.md @@ -18,7 +18,7 @@ Plejd output devices typically appears as either lights or switches in Home Assi | DIM-01 | - | Light | | | DIM-02 | - | Light | | | LED-10 | - | Light | | -| DAL-01 | - | - | Not supported, not released by Plejd | +| DAL-01 | - | - | Not tested, not supported | | WPH-01 | - | Device Automation | type:button_short_press, subtype:button_1, button_2,button_3,button_4 | | WRT-01 | - | Device Automation | type:button_short_press, subtype:button_1 | | GWY-01 | - | - | | @@ -93,7 +93,7 @@ Browse your Hass.io installation using a tool that allows you to manage files, f - Enjoy! ### Detailed Home Assistant instructions -[The details](../Details.md) +[The details](./Details.md) ### Install older versions or developemnt version From 56ab10744cce580bbf3fa4065e6a50c668077ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Wed, 9 Jun 2021 15:49:18 +0200 Subject: [PATCH 37/40] Improve documentation --- plejd/Details.md | 49 ++++++-- plejd/README.md | 296 +++++++++++++++++++++-------------------------- 2 files changed, 173 insertions(+), 172 deletions(-) diff --git a/plejd/Details.md b/plejd/Details.md index 3aabbef..ee49bfe 100644 --- a/plejd/Details.md +++ b/plejd/Details.md @@ -1,28 +1,55 @@ -# Details for Home Assistant beginners +# Details regarding installation + If you can reach your Home Assistant at [http://homeassistant.local:8123](http://homeassistant.local:8123) the links below should work. -## Mosquitto -Head over to Supervisor -> Add-on Store and search for `mosquitto broker`. -Install it and then start [mosquito addon link](http://homeassistant.local:8123/hassio/addon/core_mosquitto/info) +## Mosquitto +Head over to Supervisor -> Add-on Store and search for `mosquitto broker`. +Install it and then start [mosquito addon link](http://homeassistant.local:8123/hassio/addon/core_mosquitto/info) ## Add api user for Mosquito + Add a Home Assistant user for the Plejd addon to be able to connect to Mosquito [Configuration -> Users](http://homeassistant.local:8123/config/users) Call the user e.g. `mqtt-api-user`, set a password and save ## Plejd + Follow the `Easy Installation` in [README.MD](plejd/README.md) And `Configuration Parameters` on the same page. -The only parameters needing a value are - * site - * username - * password - * mqttUsername e.g. `mqtt-api-user` - * mqttPassword +The only parameters needing a value are + +- site +- username +- password +- mqttUsername e.g. `mqtt-api-user` +- mqttPassword Now you can start the Plejd add-on ## Where are the lights? + Head over to [Configuration -> Integrations](http://homeassistant.local:8123/config/integrations) and click Configure on MQTT After this step a new `Mosquito broker` should appear on the same page. If everything was setup correctly. It will list your lights under -`1 entity`/`n entities` \ No newline at end of file +`1 entity`/`n entities` + +## Running the Plejd add-on in VirtualBox on Windows + +If on Windows + VirtualBox or similar setup + +- Install VirtualBox extensions to get USB 2/3 +- Redirect correct USB device +- Potentially try to replace BT drivers with WinUSB using Zadig +- (Re)start VirtualBox HA machine + +## Running the Plejd add-on outside of Home Assistant Operating System ("HassOS") + +If you're planning on running this add-on outside of HassOS, you might need to turn off AppArmor in the `config.json` file. This is due to missing AppArmor configuration that is performed in HassOS (if you've manually done it, ignore this). + +Open the `config.json` file and locate `host_dbus`, after that line, insert: `"apparmor": "no",` and then restart the add-on. + +More information about available parameters can be found here: +https://developers.home-assistant.io/docs/en/hassio_addon_config.html + +## Migration from 32bit to 64 bit + +If you restore a backup from a 32bit system to a new 64bit system, use the Rebuild option in the Add-on diff --git a/plejd/README.md b/plejd/README.md index 4661580..180cf25 100644 --- a/plejd/README.md +++ b/plejd/README.md @@ -3,6 +3,131 @@ Hass.io add-on for Plejd home automation devices. Gives you the ability to control the Plejd home automation devices through Home Assistant. It uses MQTT to communicate with Home Assistant and supports auto discovery of the devices in range. Changed made in the Plejd app are propagated to Home Assistant. +Thanks to [ha-plejd](https://github.com/klali/ha-plejd) for inspiration. + +Disclaimer: +I am in no way affiliated with Plejd and am solely doing this as a hobby project. + +**Did you like this? Consider helping me continue the development:** +[Buy me a coffee](https://www.buymeacoffee.com/w1ANTUb) + +[![Gitter](https://badges.gitter.im/hassio-plejd/community.svg)](https://gitter.im/hassio-plejd/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +## Getting started + +To get started, make sure that the following requirements are met: + +### Requirements + +- A Bluetooth device (BLE), se "Tested on" section below. +- An MQTT broker (the [Mosquitto broker Home Assistant add-on](https://github.com/home-assistant/addons/blob/master/mosquitto/DOCS.md) works perfectly well). + +### Tested on + +The add-on has been tested on the following platforms: + +- Odroid-n2+ / Home Assistant Blue / Home Assistant Operating System ("HassOS") / BT ASUS USB-BT400 - Chipset: Broadcom BCM20702A1-0b05-17cb +- Raspberry Pi 4 with Home Assistant ("Hass.io") / Built-in BT +- Raspberry Pi 4 with Home Assistant ("Hass.io"/aarch64) / Built-in BT +- Intel NUC7i5BNH with Home Assistant Operating System ("HassOS") intel NUC image / Built-in BT +- Windows 10 Pro host / Oracle VirtualBox 6.1 / Home Assistant VBox image / Deltaco BT-118 with Cambridge Silicon Radio chipset / Windows + Zadig to change driver to WinUSB +- Windows 10 host / Oracle Virtualbox 6.1 / Home Assistant VBox image / ASUS BT400 +- Mac OS Catalina 10.15.1 with Node v. 13.2.0 + +Supported Plejd devices are detailed in a specific "Plejd devices" section below. + +### Easy Installation + +Browse to your Home Assistant installation in a web browser and click on `Supervisor` in the navigation bar to the left. + +- Open the Home Assistant web console and click `Supervisor` in the menu on the left side. +- Click on `Add-on Store` in the top navigation bar of that page. +- Click on the three vertical dots to the far right and chose `Repositories` +- Paste the URL to this repo https://github.com/icanos/hassio-plejd.git in the `Add` field and hit `Add`. +- Scroll down and you should find a Plejd add-on that can be installed. Open that and install. +- Configure hassio-plejd (see below). +- Enjoy! + +### Manual Installation + +Browse your Home Assistant installation using a tool that allows you to manage files, for eg. SCP, SMB, SFTP client, etc. + +- Open the `/addon` directory +- Create a new folder named `hassio-plejd` +- Copy all files from this repository into that newly created one. +- Open the Home Assistant web console and click `Supervisor` in the menu on the left side. +- Click on `Add-on Store` in the top navigation bar of that page. +- Click on the refresh button in the upper right corner. +- A new Local Add-on should appear named Plejd. Open that and install. +- Enjoy! + +### Install older versions or development version + +To install older versions, follow the "Manual Installation" instructions above, but copy the code from [one of the releases](https://github.com/icanos/hassio-plejd/releases). To test new functionality you can download the development version, available in the [develop branch](https://github.com/icanos/hassio-plejd/tree/develop). + +### More details regarding installation + +Please look at [The details](./Details.md) separate document for more detailed instructions regarding Home Asssistant, Mosquitto, etc. + +### Startup error message + +When starting the add-on, the log displays this message: + +``` +parse error: Expected string key before ':' at line 1, column 4 +[08:56:24] ERROR: Unknown HTTP error occured +``` + +However, the add-on still works as expected and this is something I'm looking into, but not with that much effort yet though. + +## Configuration + +### Simple MQTT Configurations + +When you are using the official Mosquitto Broker from Home Assistant Add-on store, minimal configuration is required. +Create a user in [Configuration -> Users](http://homeassistant.local:8123/config/users) named e.g. mqtt-api-user + +| Parameter | Value | +| ------------ | ------------------------------------------------ | +| mqttBroker | mqtt:// | +| mqttUsername | Arbitrary Home Assistant User e.g. mqtt-api-user | +| mqttPassword | Users password | + +### Advanced MQTT Configurations + +For more advanced instllations, you need to add the MQTT integration to Home Assistant either by going to Configuration -> Integrations and clicking the Add Integration button, or by adding the following to your `configuration.yaml` file: + +``` +mqtt: + broker: [point to your broker IP eg. 'mqtt://localhost'] + username: [username of mqtt broker] + password: !secret mqtt_password + discovery: true + discovery_prefix: homeassistant +``` + +The above is used to notify the add-on when Home Assistant has started successfully and let the add-on send the discovery response (containing information about all Plejd devices found). + +### Configuration Parameters + +The plugin needs you to configure some settings before working. You find these on the Add-on page after you've installed it. + +| Parameter | Value | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| site | Name of your Plejd site, the name is displayed in the Plejd app (top bar). | +| username | Username of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. | +| password | Password of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. | +| mqttBroker | URL of the MQTT Broker, eg. mqtt:// | +| mqttUsername | Username of the MQTT broker | +| mqttPassword | Password of the MQTT broker | +| includeRoomsAsLights | Adds all rooms as lights, making it possible to turn on/off lights by room instead. Setting this to false will ignore all rooms. | +| updatePlejdClock | Hourly update Plejd devices' clock if out of sync. Clock is used for time-based scenes. Not recommended if you have a Plejd gateway. Clock updates may flicker scene-controlled devices. | +| logLevel | Minimim log level. Supported values are `error`, `warn`, `info`, `debug`, `verbose`, `silly` with increasing amount of logging. Do not log more than `info` for production purposes. | +| connectionTimeout | Number of seconds to wait when scanning and connecting. Might need to be tweaked on platforms other than RPi 4. Defaults to: 2 seconds. | +| writeQueueWaitTime | Wait time between message sent to Plejd over BLE, defaults to 400. If that doesn't work, try changing the value higher in steps of 50. | + +## Plejd devices and corresponding Home Assistant devices + Plejd output devices typically appears as either lights or switches in Home Assistant depending on how they are configured. | Device | Plejd Configuration | Home Assistant Role | Comment | @@ -28,157 +153,21 @@ Plejd output devices typically appears as either lights or switches in Home Assi | Room | - | Area | Can be changed by Home Assistant | | Room | - | Light | If includeRoomsAsLights is set to true | -Thanks to [ha-plejd](https://github.com/klali/ha-plejd) for inspiration. +## Transitions -Disclaimer: -I am in no way affiliated with Plejd and am solely doing this as a hobby project. +Transitions from Home Assistant are supported (for dimmable devices) when transition is longer than 1 second. Plejd will do a bit of internal transitioning (default soft start is 0.1 seconds). -**Did you like this? Consider helping me continue the development:** -[Buy me a coffee](https://www.buymeacoffee.com/w1ANTUb) +This implementation will transition each device independently, meaning that brightness change might be choppy if transitioning many devices at once or a changing brightness a lot in a limited time. Hassio-plejd's communication channel seems to handle a few updates per second, this is the combined value for all devices. -[![Gitter](https://badges.gitter.im/hassio-plejd/community.svg)](https://gitter.im/hassio-plejd/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +Transition points will be skipped if the queue of messages to be sent is over a certain threshold, by default equal to the number of devices in the system. Total transition time is prioritized rather than smoothness. -## Getting started +Recommendations -To get started, make sure that the following requirements are met: - -### Requirements - -- A Bluetooth device (BLE), for eg. the built-in device in Raspberry Pi 4. -- An MQTT broker (the Mosquitto Hass.io add-on works perfectly well). - -### Tested on - -The add-on has been tested on the following platforms: - -- Mac OS Catalina 10.15.1 with Node v. 13.2.0 -- Raspberry Pi 4 with Hass.io -- Raspberry Pi 4 with Hass.io/aarch64 -- Intel NUC7i5BNH with HassOS intel NUC image (built-in BT) - -#### Tested Plejd devices - -- DIM-01 -- DIM-02 -- LED-10 -- CTR-01 -- REL-01 -- REL-02 -- WPH-01 -- WRT-01 - -### Easy Installation - -Browse to your Home Assistant installation in a web browser and click on `Supervisor` in the navigation bar to the left. - -- Open the Home Assistant web console and click `Supervisor` in the menu on the left side. -- Click on `Add-on Store` in the top navigation bar of that page. -- Click on the three vertical dots to the far right and chose `Repositories` -- Paste the URL to this repo https://github.com/icanos/hassio-plejd.git in the `Add` field and hit `Add`. -- Scroll down and you should find a Plejd add-on that can be installed. Open that and install. -- Configure hassio-plejd (see below). -- Enjoy! - -### Manual Installation - -Browse your Hass.io installation using a tool that allows you to manage files, for eg. SMB or an SFTP client etc. - -- Open the `/addon` directory -- Create a new folder named `hassio-plejd` -- Copy all files from this repository into that newly created one. -- Open the Home Assistant web console and click `Supervisor` in the menu on the left side. -- Click on `Add-on Store` in the top navigation bar of that page. -- Click on the refresh button in the upper right corner. -- A new Local Add-on should appear named Plejd. Open that and install. -- Enjoy! - -### Detailed Home Assistant instructions -[The details](./Details.md) - -### Install older versions or developemnt version - -To install older versions, follow the "Manual Installation" instructions above, but copy the code from [one of the releases](https://github.com/icanos/hassio-plejd/releases). To test new functionality you can download the development version, available in the [develop branch](https://github.com/icanos/hassio-plejd/tree/develop). - -### IMPORTANT INFORMATION - -#### Startup error message - -When starting the add-on, the log displays this message: - -``` -parse error: Expected string key before ':' at line 1, column 4 -[08:56:24] ERROR: Unknown HTTP error occured -``` - -However, the add-on still works as expected and this is something I'm looking into, but not with that much effort yet though. - -#### Running the Plejd add-on in VirtualBox on Windows - -If on Windows + VirtualBox or similar setup - -- Install VirtualBox extensions to get USB 2/3 -- Redirect correct USB device -- Potentially try to replace BT drivers with WinUSB using Zadig -- (Re)start VirtualBox HA machine - -#### Running the Plejd add-on outside of HassOS - -If you're planning on running this add-on outside of HassOS, you might need to turn off AppArmor in the `config.json` file. This is due to missing AppArmor configuration that is performed in HassOS (if you've manually done it, ignore this). - -Open the `config.json` file and locate `host_dbus`, after that line, insert: `"apparmor": "no",` and then restart the add-on. - -More information about available parameters can be found here: -https://developers.home-assistant.io/docs/en/hassio_addon_config.html - -#### Migration from 32bit to 64 bit - -If you restore a backup from a 32bit system to a new 64bit system, use the Rebuild option in the Add-on - -### Configuration - -#### Simple MQTT Configurations - -When you are using the official Mosquitto Broker from Home Assistant Add-on store, minimal configuration is required. -Create a user in [Configuration -> Users](http://homeassistant.local:8123/config/users) named e.g. mqtt-api-user - -| Parameter | Value | -| ------------ | ---------------------------------------------------------------------------------------------------------------- | -| mqttBroker | mqtt:// | -| mqttUsername | Arbitrary Home Assistant User e.g. mqtt-api-user | -| mqttPassword | Users password | - -#### Advanced MQTT Configurations - -For more advanced instllations, you need to add the MQTT integration to Home Assistant either by going to Configuration -> Integrations and clicking the Add Integration button, or by adding the following to your `configuration.yaml` file: - -``` -mqtt: - broker: [point to your broker IP eg. 'mqtt://localhost'] - username: [username of mqtt broker] - password: !secret mqtt_password - discovery: true - discovery_prefix: homeassistant -``` - -The above is used to notify the add-on when Home Assistant has started successfully and let the add-on send the discovery response (containing information about all Plejd devices found). - -#### Configuration Parameters - -The plugin needs you to configure some settings before working. You find these on the Add-on page after you've installed it. - -| Parameter | Value | -| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| site | Name of your Plejd site, the name is displayed in the Plejd app (top bar). | -| username | Username of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. | -| password | Password of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. | -| mqttBroker | URL of the MQTT Broker, eg. mqtt:// | -| mqttUsername | Username of the MQTT broker | -| mqttPassword | Password of the MQTT broker | -| includeRoomsAsLights | Adds all rooms as lights, making it possible to turn on/off lights by room instead. Setting this to false will ignore all rooms. | -| updatePlejdClock | Hourly update Plejd devices' clock if out of sync. Clock is used for time-based scenes. Not recommended if you have a Plejd gateway. Clock updates may flicker scene-controlled devices. | -| logLevel | Minimim log level. Supported values are `error`, `warn`, `info`, `debug`, `verbose`, `silly` with increasing amount of logging. Do not log more than `info` for production purposes. | -| connectionTimeout | Number of seconds to wait when scanning and connecting. Might need to be tweaked on platforms other than RPi 4. Defaults to: 2 seconds. | -| writeQueueWaitTime | Wait time between message sent to Plejd over BLE, defaults to 400. If that doesn't work, try changing the value higher in steps of 50. | +- Only transition a few devices at a time when possible +- Entire rooms can be transitioned efficiently after settin gincludeRoomsAsLights to true +- Expect 5-10 brightness changes per second, meaning 5 devices => 1-2 updates per device per second +- ... meaning that SLOW transitions will work well (wake-up light, gradually fade over a minute, ...), but quick ones will only work well for few devices or small relative changes in brightness +- When experiencing choppy quick transitions, turn transitioning off and let the Plejd hardware do the work instead ## Troubleshooting @@ -202,21 +191,6 @@ If you're having issues to get the addon working, there are a few things you can - One Plejd device means max one BLE connection, meaning using the Plejd app over BT will disconnect the addon BLE connection - It seems you can kick yourself out (by connecting using the app) even when you have multiple devices if the app happens to connect to the same device as the addon is using -## Transitions - -Transitions from Home Assistant are supported (for dimmable devices) when transition is longer than 1 second. Plejd will do a bit of internal transitioning (default soft start is 0.1 seconds). - -This implementation will transition each device independently, meaning that brightness change might be choppy if transitioning many devices at once or a changing brightness a lot in a limited time. Hassio-plejd's communication channel seems to handle a few updates per second, this is the combined value for all devices. - -Transition points will be skipped if the queue of messages to be sent is over a certain threshold, by default equal to the number of devices in the system. Total transition time is prioritized rather than smoothness. - -Recommendations - -- Only transition a few devices at a time when possible -- Expect 5-10 brightness changes per second, meaning 5 devices => 1-2 updates per device per second -- ... meaning that SLOW transitions will work well (wake-up light, gradually fade over a minute, ...), but quick ones will only work well for few devices or small relative changes in brightness -- When experiencing choppy quick transitions, turn transitioning off and let the Plejd hardware do the work instead - ## I want voice control! With the Google Home integration in Home Assistant, you can get voice control for your Plejd lights right away, check this out for more information: From afbf5df5b83cf274afd13d4f88642ea4db29f3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Fri, 11 Jun 2021 14:11:55 +0200 Subject: [PATCH 38/40] Update changelog for 0.8.0-beta release --- plejd/CHANGELOG.md | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/plejd/CHANGELOG.md b/plejd/CHANGELOG.md index 6491c1c..767a5e1 100644 --- a/plejd/CHANGELOG.md +++ b/plejd/CHANGELOG.md @@ -1,10 +1,12 @@ # Changelog hassio-plejd Home Assistant Plejd addon -## 0.8.0-dev +## [0.8.0-beta](https://github.com/icanos/hassio-plejd/tree/0.8.0-beta) (2021-06-14) + +[Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.7.1...0.8.0-beta) **BREAKING - READ BELOW FIRST** -Release 0.8 will break ALL EXISTING DEVICES. Unique mqtt id:s will change, meaning HA will create new devices. Scenes will be added as scenes not as switches. Devices will be installed to Areas named by the rooms defined in the Plejd app (can be changed) +Release 0.8 and later will break ALL EXISTING DEVICES from earlier versions. Unique mqtt id:s will change, meaning HA will create new devices. Scenes will be added as scenes not as switches. Devices will be installed to Areas named by the rooms defined in the Plejd app (can be changed) Recommendations to minimize impact @@ -14,6 +16,30 @@ Recommendations to minimize impact - Go to Configuration => Integration => MQTT. Go to entities and after that devices and remove all Plejd devices (should be listed as unavailable) - Upgrade addon to latest version and start - All devices should now be back. With luck they will have the same HA id:s as before so most things should work. Room assignments, icons, automations, scenes, etc will have to be gone though. +- If all else fails you can uninstall the Plejd addon and the Mqtt addon (which should remove all Mqtt devices after restart), re-install and get back the same device id:s as you had before. + +**Closed issues:** + +- Configuration instruction outdated [\#189](https://github.com/icanos/hassio-plejd/issues/189) +- Cant turn on lights after update [\#183](https://github.com/icanos/hassio-plejd/issues/183) +- Discovery finds lights but claims not to [\#182](https://github.com/icanos/hassio-plejd/issues/182) +- MQTTS connection problems with mqtt@~3.0.0 [\#181](https://github.com/icanos/hassio-plejd/issues/181) +- Adding repository to HACS [\#180](https://github.com/icanos/hassio-plejd/issues/180) +- WPH-01 buttons to trigger generic automations in HA [\#172](https://github.com/icanos/hassio-plejd/issues/172) +- Scene id and device id can overlap meaning mqtt commands overlap [\#161](https://github.com/icanos/hassio-plejd/issues/161) +- Add to "Tested on" section [\#122](https://github.com/icanos/hassio-plejd/issues/122) +- USB Bluetooth adapter [\#101](https://github.com/icanos/hassio-plejd/issues/101) +- Ignores devices if they have same name [\#91](https://github.com/icanos/hassio-plejd/issues/91) +- Scene does not change state [\#85](https://github.com/icanos/hassio-plejd/issues/85) + +**Merged pull requests:** + +- Added more documentation to install steps [\#201](https://github.com/icanos/hassio-plejd/pull/201) ([polyzois](https://github.com/polyzois)) +- Fix for issue discussed in \#198. [\#199](https://github.com/icanos/hassio-plejd/pull/199) ([faanskit](https://github.com/faanskit)) +- Suggested Area and fix for \#189 [\#192](https://github.com/icanos/hassio-plejd/pull/192) ([faanskit](https://github.com/faanskit)) +- Support for WPH-01 and WRT-01 added. [\#188](https://github.com/icanos/hassio-plejd/pull/188) ([faanskit](https://github.com/faanskit)) +- Refactor unique id handling throughout the addon [\#179](https://github.com/icanos/hassio-plejd/pull/179) ([SweVictor](https://github.com/SweVictor)) +- Update README.md [\#178](https://github.com/icanos/hassio-plejd/pull/178) ([zissou1](https://github.com/zissou1)) ## [0.7.1](https://github.com/icanos/hassio-plejd/tree/0.7.1) (2021-03-25) @@ -23,6 +49,9 @@ Recommendations to minimize impact - Can't connect to device: TypeError: Cannot read property 'dimmable' [\#175](https://github.com/icanos/hassio-plejd/issues/175) +**Merged pull requests:** +- Release 0.7.1 [\#177](https://github.com/icanos/hassio-plejd/pull/177) ([SweVictor](https://github.com/SweVictor)) + ## [0.7.0](https://github.com/icanos/hassio-plejd/tree/0.7.0) (2021-03-23) [Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.6.2...0.7.0) From 2f96065beceb2ef386e33f6663da1fd31283e613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 14 Jun 2021 10:22:59 +0200 Subject: [PATCH 39/40] Added RPI 3+ to Tested on section --- plejd/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plejd/README.md b/plejd/README.md index 180cf25..7fbca72 100644 --- a/plejd/README.md +++ b/plejd/README.md @@ -29,6 +29,7 @@ The add-on has been tested on the following platforms: - Odroid-n2+ / Home Assistant Blue / Home Assistant Operating System ("HassOS") / BT ASUS USB-BT400 - Chipset: Broadcom BCM20702A1-0b05-17cb - Raspberry Pi 4 with Home Assistant ("Hass.io") / Built-in BT - Raspberry Pi 4 with Home Assistant ("Hass.io"/aarch64) / Built-in BT +- Raspberry Pi 3+ with Home Assistant ("Hass.io") / Built-in BT - Intel NUC7i5BNH with Home Assistant Operating System ("HassOS") intel NUC image / Built-in BT - Windows 10 Pro host / Oracle VirtualBox 6.1 / Home Assistant VBox image / Deltaco BT-118 with Cambridge Silicon Radio chipset / Windows + Zadig to change driver to WinUSB - Windows 10 host / Oracle Virtualbox 6.1 / Home Assistant VBox image / ASUS BT400 From 2305d149bc13c970f7f24f329d4a7e6d371e0d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 14 Jun 2021 10:28:45 +0200 Subject: [PATCH 40/40] Set version to 0.8.0-beta to prepare for relase --- plejd/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plejd/config.json b/plejd/config.json index 3b0e94f..b9b2330 100644 --- a/plejd/config.json +++ b/plejd/config.json @@ -1,6 +1,6 @@ { "name": "Plejd", - "version": "0.8.0-dev", + "version": "0.8.0-beta", "slug": "plejd", "description": "Adds support for the Swedish home automation devices from Plejd.", "url": "https://github.com/icanos/hassio-plejd/",