From 09c2a65cc5089081c225e4817031d17a704dbde1 Mon Sep 17 00:00:00 2001 From: sguernion Date: Mon, 29 Jan 2024 21:52:27 +0100 Subject: [PATCH] override info with device settings --- .github/workflows/gh-pages.yml | 1 + .github/workflows/main.yml | 2 + config.sample.yml | 74 ++++++++--------- .../usage/mqtt_topics_and_messages.md | 50 +++++------ src/libs/Controller.ts | 21 +++-- src/libs/Discovery.ts | 83 ++++++++++++++----- src/libs/RfxcomBridge.ts | 28 +++++-- src/libs/Settings.ts | 8 +- 8 files changed, 165 insertions(+), 102 deletions(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index b360b2d..6a4d2e2 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -31,6 +31,7 @@ jobs: with: node-version: 20 cache: pnpm + cache-dependency-path: './documentation/package-lock.json' - name: Build Blog env: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6653a41..293857f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,6 +15,8 @@ jobs: uses: docker/metadata-action@v5 with: images: sguernion/rfxcom2mqtt + tags: | + type=raw,value=latest,enable={{is_default_branch}} - name: Set up QEMU uses: docker/setup-qemu-action@v2 diff --git a/config.sample.yml b/config.sample.yml index 773405f..36d715b 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -47,45 +47,45 @@ rfxcom: - remote - security1 -devices: - - id: '0x5C02' - title: 'Bathroom Temp & Hum' + devices: + - id: '0x5C02' + friendlyName: 'Bathroom Temp & Hum' - - id: '0xB9459A' - title: 'Garden motion' + - id: '0xB9459A' + friendlyName: 'Garden motion' - - id: '1001010/1' - name: 'CucuDimmer' - title: 'Kitchen Dimmer Light' - type: 'lighting2' + - id: '1001010/1' + name: 'CucuDimmer' + friendlyName: 'Kitchen Dimmer Light' + type: 'lighting2' - - id: '0x012E00FF' - name: 'Living Room switch' + - id: '0x012E00FF' + friendlyName: 'Living Room switch' - - id: '0x00ED400F' - name: 'Lights' - units: - - unitCode: '1' - name: 'Light1' - title: 'Living Room' - - unitCode: '2' - name: 'Light2' - title: 'Kitchen' - - unitCode: '3' - name: 'Light3' - title: 'Garage' - - unitCode: '4' - name: 'Light4' - title: 'Garden' - type: 'lighting2' + - id: '0x00ED400F' + name: 'Lights' + units: + - unitCode: '1' + name: 'Light1' + friendlyName: 'Living Room' + - unitCode: '2' + name: 'Light2' + friendlyName: 'Kitchen' + - unitCode: '3' + name: 'Light3' + friendlyName: 'Garage' + - unitCode: '4' + name: 'Light4' + friendlyName: 'Garden' + type: 'lighting2' - - id: '0x3D090F' - name: 'Switch1' - command: 'on' - title: 'Living Room Lights on' - type: 'lighting4' - - id: '0x3D090E' - name: 'Switch1' - command: 'off' - title: 'Living Room Lights off' - type: 'lighting4' + - id: '0x3D090F' + name: 'Switch1' + command: 'on' + friendlyName: 'Living Room Lights on' + type: 'lighting4' + - id: '0x3D090E' + name: 'Switch1' + command: 'off' + friendlyName: 'Living Room Lights off' + type: 'lighting4' diff --git a/documentation/usage/mqtt_topics_and_messages.md b/documentation/usage/mqtt_topics_and_messages.md index 29b462b..7256f7f 100644 --- a/documentation/usage/mqtt_topics_and_messages.md +++ b/documentation/usage/mqtt_topics_and_messages.md @@ -38,43 +38,39 @@ Contains the state of the bridge, this message is published as retained. Payload Contains information of an device. -Example payload on topic `"rfxcom2mqtt/devices/0x5C02"`: +Example payload on topic `"rfxcom2mqtt/devices/0x01A4F9BE/2"`: ``` - { - "title": "Bathroom Temp & Hum", - "type":"temperaturehumidity1", - "subtype": 13, - "id": "0x5C03", - "seqnbr": 12, - "temperature": 18, - "humidity": 74, - "humidityStatus": 3, - "batteryLevel": 9, - "rssi": 6 - } + { + "seqnbr": 4, + "subtype": 0, + "id": "0x01A4F9BE", + "unitCode": 2, + "commandNumber": 0, + "command": "Off", + "level": 0, + "rssi": 4, + "type": "lighting2", + "deviceName": [ + "KlikAanKlikUit", + "HomeEasy UK", + "Chacon", + "NEXA", + "Intertechno" + ], + "subTypeValue": "AC" + } ``` ### Publish command examples (topic/payload) ``` - rfxcom2mqtt/commmand/CucuDimmer + rfxcom2mqtt/cmd/lighting2/0/0x01A4F9BE/2/set on - rfxcom2mqtt/commmand/CucuDimmer + rfxcom2mqtt/cmd/lighting2/0/0x01A4F9BE/2/set off - rfxcom2mqtt/commmand/CucuDimmer + rfxcom2mqtt/cmd/lighting2/0/0x01A4F9BE/2/set level 15 - rfxcom2mqtt/commmand/Switch1 (lighting4, payload identifies device) - on - - rfxcom2mqtt/commmand/Switch1 - off - - rfxcom2mqtt/commmand/Lights/Light1 (lighting2, unitName identifies device) - on - - rfxcom2mqtt/commmand/Lights/Light1 - off ``` \ No newline at end of file diff --git a/src/libs/Controller.ts b/src/libs/Controller.ts index 3010173..ad2fb3d 100644 --- a/src/libs/Controller.ts +++ b/src/libs/Controller.ts @@ -1,5 +1,5 @@ -import {Settings,read} from './Settings'; +import {Settings, SettingDevice, read} from './Settings'; import Discovery from './Discovery'; import Mqtt from './Mqtt'; import Rfxcom, {IRfxcom,MockRfxcom} from './RfxcomBridge'; @@ -25,7 +25,7 @@ export default class Controller implements MqttEventListener{ this.config = read(file); logger.setLevel(this.config.loglevel); logger.info("configuration : "+JSON.stringify(this.config)); - this.rfxBridge = this.config.mock ? new MockRfxcom() : new Rfxcom(this.config.rfxcom); + this.rfxBridge = this.config.mock ? new MockRfxcom(this.config.rfxcom) : new Rfxcom(this.config.rfxcom); this.mqttClient = new Mqtt(this.config) this.discovery = new Discovery( this.mqttClient, this.rfxBridge, this.config ); this.mqttClient.addListener(this.discovery); @@ -120,7 +120,7 @@ export default class Controller implements MqttEventListener{ let entityName = dn[3]; // Used for units and forms part of the device id if (dn[4] !== undefined && dn[4].length > 0) { - entityName = entityName + '/' + dn[4]; + entityName += '/' + dn[4]; } this.rfxBridge.onCommand(deviceType, entityName, data.message); return; @@ -131,7 +131,7 @@ export default class Controller implements MqttEventListener{ } - sendToMQTT(type: any, evt: any,deviceConf: any) { + sendToMQTT(type: any, evt: any,deviceConf?: SettingDevice) { logger.info("receive from rfxcom : "+JSON.stringify(evt)); // Add type to event! evt.type = type; @@ -145,10 +145,8 @@ export default class Controller implements MqttEventListener{ let topicEntity = deviceId; // Get device config if available - if (deviceConf instanceof Object) { - if (deviceConf.name !== undefined) { + if (deviceConf?.name !== undefined) { topicEntity = deviceConf.name; - } } const json = JSON.stringify(evt, null, 2); @@ -156,6 +154,15 @@ export default class Controller implements MqttEventListener{ if(payload.unitCode !== undefined && !this.rfxBridge.isGroup(payload)){ topicEntity += '/' + payload.unitCode; + if (deviceConf?.units) { + deviceConf?.units.forEach( unit => { + if(parseInt(unit.unitCode) === parseInt(payload.unitCode)){ + if (unit.name !) { + topicEntity = unit.name; + } + } + }); + } } this.mqttClient.publish(this.mqttClient.topics.devices + '/' + topicEntity, json, (error: any) => {}); diff --git a/src/libs/Discovery.ts b/src/libs/Discovery.ts index d52d9bc..f16aa1f 100644 --- a/src/libs/Discovery.ts +++ b/src/libs/Discovery.ts @@ -2,7 +2,7 @@ var rfxcom = require('rfxcom'); import {IRfxcom} from './RfxcomBridge'; -import {Settings, SettingHass} from './Settings'; +import {Settings, SettingHass, SettingDevice} from './Settings'; import Mqtt from './Mqtt'; import { DeviceEntity, DeviceBridge,BridgeInfo,MQTTMessage,MqttEventListener } from './models'; import utils from './utils'; @@ -92,9 +92,11 @@ export default class Discovery implements MqttEventListener{ export class HomeassistantDiscovery extends AbstractDiscovery{ protected state: State; + protected devicesConfig: SettingDevice[]; constructor(mqtt: Mqtt, rfxtrx: IRfxcom, config : Settings){ super(mqtt, rfxtrx, config); + this.devicesConfig = config.rfxcom.devices; this.state = new State(config); } @@ -132,25 +134,43 @@ export class HomeassistantDiscovery extends AbstractDiscovery{ // get from save state let entityState = this.state.get({id: entityName,type:deviceType,subtype:data.message.subtype}) + entityState.deviceType = deviceType; this.updateEntityStateFromValue(entityState,value); - this.rfxtrx.sendCommand(deviceType,subTypeValue,entityState.rfxFunction,id+"/"+unitCode); - this.mqtt.publish(this.mqtt.topics.devices + '/' + entityTopic, JSON.stringify(entityState), (error: any) => {},{retain: true, qos: 1}); + this.rfxtrx.sendCommand(deviceType,subTypeValue,entityState.rfxFunction,entityTopic); + this.mqtt.publish(this.mqtt.topics.devices + '/' + entityName, JSON.stringify(entityState), (error: any) => {},{retain: true, qos: 1}); } updateEntityStateFromValue(entityState: any,value: string){ - if( entityState.type === 'lighting1' || entityState.type === 'lighting2' || entityState.type === 'lighting3' - || entityState.type === 'lighting5' || entityState.type === 'lighting6') { - if (value === "On" || value === "Group On") { - //TODO load value from rfxcom commands - entityState.commandNumber = (value === "Group On")?4:1; + if( entityState.deviceType === 'lighting1' || entityState.deviceType === 'lighting2' || entityState.deviceType === 'lighting3' + || entityState.deviceType === 'lighting5' || entityState.deviceType === 'lighting6' ) { + const cmd = value.toLowerCase().split(" ") + let command = cmd[0]; + if (cmd[0] === "group") { + command = cmd[1]; + + } + if (command === "on") { + entityState.commandNumber = (cmd[0] === "group")?4:1; //WORK only for lithing2 entityState.rfxFunction = 'switchOn'; - } else { - entityState.commandNumber = (value === "Group On")?3:0; - entityState.rfxFunction = 'switchOff'; + } else if (command === "off") { + entityState.rfxFunction = (cmd[0] === "group")?3:0; //WORK only for lithing2 + entityState.rfxCommand = 'switchOff'; + }else{ + if (cmd[0] === "level") { + entityState.rfxFunction = 'setLevel'; + entityState.rfxOpt = cmd[1]; + } } - entityState.command = value; + }else if (entityState.deviceType === "lighting4") { + entityState.rfxFunction = 'sendData'; + }else if (entityState.deviceType === "chime1") { + entityState.rfxFunction = 'chime'; + } else { + logger.error('device type ('+entityState.deviceType+') not supported'); } + + //TODO get command for other deviceType } @@ -160,19 +180,42 @@ export class HomeassistantDiscovery extends AbstractDiscovery{ const devicePrefix = this.config.discovery_device; let id = payload.id; let deviceId = payload.subTypeValue+"_"+id.replace("0x",""); + let deviceTopic = payload.id + let deviceName = deviceId; let entityId = payload.subTypeValue+"_"+id.replace("0x",""); - let entityTopic = payload.id; let entityName = payload.id; - + let entityTopic = payload.id + + const deviceConf = this.devicesConfig.find((dev: any) => dev.id === id); + + if (deviceConf?.name !== undefined) { + entityTopic = deviceConf.name; + deviceTopic = deviceConf.name; + } + + if(payload.unitCode !== undefined && !this.rfxtrx.isGroup(payload)){ entityId += '_' + payload.unitCode; entityTopic += '/'+ payload.unitCode; entityName += '_'+payload.unitCode; + if (deviceConf?.units) { + deviceConf?.units.forEach( unit => { + if(parseInt(unit.unitCode) === parseInt(payload.unitCode)){ + if (unit.name !) { + entityTopic = unit.name; + } + } + }); + } } this.state.set({id: entityName,type:payload.type,subtype:payload.subtype},payload,"event"); - const deviceJson = new DeviceEntity([devicePrefix+'_'+deviceId],deviceId); + if (deviceConf?.friendlyName) { + deviceName = deviceConf?.friendlyName; + } + + const deviceJson = new DeviceEntity([devicePrefix+'_'+deviceId,devicePrefix+'_'+deviceName],deviceName); if( payload.rssi !== undefined ){ const json = { @@ -182,16 +225,16 @@ export class HomeassistantDiscovery extends AbstractDiscovery{ entity_category: "diagnostic", icon: "mdi:signal", json_attributes_topic: this.topicDevice + '/' + entityTopic, - name: deviceId+" Linkquality", - object_id: deviceId+'_linkquality', + name: deviceName+" Linkquality", + object_id: deviceTopic+'_linkquality', origin: this.discoveryOrigin, state_class: "measurement", state_topic: this.topicDevice + '/' + entityTopic, - unique_id: deviceId +'_linkquality_' + devicePrefix, + unique_id: deviceTopic +'_linkquality_' + devicePrefix, unit_of_measurement:"dBm", value_template: "{{ value_json.rssi }}" }; - this.publishDiscovery('sensor/' + deviceId +'/linkquality/config',JSON.stringify(json)); + this.publishDiscovery('sensor/' + deviceTopic +'/linkquality/config',JSON.stringify(json)); } if( payload.type === 'lighting1' || payload.type === 'lighting2' || payload.type === 'lighting3' || payload.type === 'lighting5' || payload.type === 'lighting6' ){ @@ -221,7 +264,7 @@ export class HomeassistantDiscovery extends AbstractDiscovery{ unique_id: entityId+'_'+devicePrefix, value_template:"{{ value_json.command }}" }; - this.publishDiscovery('switch/' + entityId +'/config',JSON.stringify(json)); + this.publishDiscovery('switch/' + entityTopic +'/config',JSON.stringify(json)); } } diff --git a/src/libs/RfxcomBridge.ts b/src/libs/RfxcomBridge.ts index 73189ec..5ffc80f 100644 --- a/src/libs/RfxcomBridge.ts +++ b/src/libs/RfxcomBridge.ts @@ -1,6 +1,6 @@ var rfxcom = require('rfxcom'); -import { SettingRfxcom } from './Settings'; +import { SettingRfxcom,SettingDevice } from './Settings'; import { RfxcomInfo } from './models'; import logger from './logger'; @@ -19,6 +19,13 @@ export interface IRfxcom{ } export class MockRfxcom implements IRfxcom{ + + private config: SettingRfxcom; + + constructor(config: SettingRfxcom){ + this.config = config + } + initialise(): Promise{ return new Promise((resolve, reject) => { logger.info('RFXCOM Mock device initialised'); @@ -49,17 +56,19 @@ export class MockRfxcom implements IRfxcom{ } subscribeProtocolsEvent(callback: any){ logger.info('RFXCOM Mock subscribeProtocolsEvent'); - callback('lighting2', {id:'mocked_device2', + const deviceId = 'mocked_device2'; + let deviceConf = this.config.devices.find((dev: any) => dev.id === deviceId); + callback('lighting2', {id:deviceId, "seqnbr": 7, "subtype": 0, - "unitCode": 2, + "unitCode": "1", "commandNumber": 0, "command": "Off", "level": 0, "rssi": 5, "type": "lighting2", "subTypeValue": "AC" - }, undefined); + }, deviceConf); } isGroup(payload: any): boolean { if(payload.type === 'lighting2'){ @@ -102,6 +111,12 @@ export default class Rfxcom implements IRfxcom{ if(payload.type === 'lighting2'){ return (payload.commandNumber === 3 || payload.commandNumber === 4); } + if(payload.type === 'lighting1'){ + return (payload.commandNumber === 5 || payload.commandNumber === 6); + } + if(payload.type === 'lighting6'){ + return (payload.commandNumber === 2 || payload.commandNumber === 3); + } return false; } @@ -169,12 +184,12 @@ export default class Rfxcom implements IRfxcom{ }); } - private getDeviceConfig(deviceId: string){ + private getDeviceConfig(deviceId: string): SettingDevice | undefined{ if (this.config.devices === undefined) { return; } - return this.config.devices.find((dev: any) => dev.id === deviceId); + return this.config.devices.find((dev: SettingDevice) => dev.id === deviceId); } onCommand(deviceType: string, entityName: string, payload: any){ @@ -220,7 +235,6 @@ export default class Rfxcom implements IRfxcom{ if (deviceConf.subtype !== undefined) { subtype = deviceConf.subtype; } - transmitRepetitions = deviceConf.repetitions; diff --git a/src/libs/Settings.ts b/src/libs/Settings.ts index e05fc42..9d3e6e6 100644 --- a/src/libs/Settings.ts +++ b/src/libs/Settings.ts @@ -58,9 +58,9 @@ export interface SettingRfxcom{ } -interface SettingDevice{ +export interface SettingDevice{ id: string, - name: string, + name?: string, friendlyName?: string, type?: string, subtype?: string, @@ -69,10 +69,10 @@ interface SettingDevice{ repetitions?: number } -interface Units{ +export interface Units{ unitCode: string, + name: string, friendlyName: string, - title?: string, } export function read(file: string): Settings {