Skip to content

Commit

Permalink
added hsb light
Browse files Browse the repository at this point in the history
  • Loading branch information
mdaskalov committed Oct 30, 2024
1 parent 65f7fd7 commit 0496407
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 62 deletions.
117 changes: 68 additions & 49 deletions src/tasmotaCharacteristic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,24 @@ import { TasmotaZbBridgePlatform } from './platform';

const EXEC_TIMEOUT = 1500;

export type Mapping = {
export type SplitMapping = {
separator?: string;
index: number;
};

export type SwapMapping = {
from: string,
to: CharacteristicValue
}[];
};

export type Mapping = SplitMapping | SwapMapping[]

export type TasmotaResponse = {
topic?: string;
path?: string;
update?: boolean;
shared?: boolean;
mapping?: Mapping;
}

export type TasmotaCommand = {
Expand All @@ -34,7 +42,6 @@ export type TasmotaCharacteristicDefinition = {
set?: TasmotaCommand;
stat?: TasmotaResponse;
props?: object
mapping?: Mapping;
default?: CharacteristicValue;
};

Expand Down Expand Up @@ -95,7 +102,26 @@ export class TasmotaCharacteristic {
if (path !== undefined) {
this.log('Configure statUpdate on topic: %s %s', topic, path);
this.platform.mqttClient.subscribeTopic(topic, message => {
this.setValue('statUpdate', this.getValueByPath(message, path));
const value = this.getValueByPath(message, path);
if (value !== undefined) {
const mapping = definition.stat?.mapping ? definition.stat?.mapping : definition.get?.res?.mapping;
const hbValue = this.mapToHB(value, mapping);
if (hbValue !== undefined) {
const prevValue = this.value;
const updateAlways = definition.stat?.update === true;
const update = (value !== prevValue) || updateAlways;
if (update) {
this.value = hbValue;
this.service.getCharacteristic(this.platform.Characteristic[this.name]).updateValue(hbValue);
}
this.log('statUpdate value%s: %s (homebridge: %s), prev: %s',
update ? '' : ' (not updated)',
value,
hbValue,
prevValue,
);
}
}
});
}
}
Expand All @@ -108,38 +134,48 @@ export class TasmotaCharacteristic {
if (this.definition.get !== undefined) {
try {
const value = await this.exec(this.definition.get);
this.setValue('onGet', value);
const hbValue = this.mapToHB(value, this.definition.get.res?.mapping);
if (hbValue !== undefined) {
this.log('onGet value: %s (homebridge: %s)', value, hbValue);
this.value = hbValue;
return this.value;
}
} catch (err) {
this.platform.log.error(err as string);
throw new this.platform.api.hap.HapStatusError(HAPStatus.OPERATION_TIMED_OUT);
}
throw new this.platform.api.hap.HapStatusError(HAPStatus.OPERATION_TIMED_OUT);
}
return this.value;
}

private async onSet(value: CharacteristicValue) {
const command = this.definition.get ? this.definition.get : this.definition.set;
const payload = this.mapFromHB(value);
const command = this.definition.set ? this.definition.set : this.definition.get;
const payload = this.mapFromHB(value, command?.res?.mapping);
if (command !== undefined && payload !== undefined) {
try {
const valueToConfirm = await this.exec(command, payload);
if (valueToConfirm === payload) {
this.setValue('onSet', payload);
const confirmValue = await this.exec(command, payload);
const mapping = this.definition.get?.res?.mapping ? this.definition.get?.res?.mapping : this.definition.set?.res?.mapping;
const hbConfirmValue = this.mapToHB(confirmValue, mapping);
if (value === hbConfirmValue) {
this.log('onSet value: %s (tasmota: %s)', value, payload);
return;
} else {
this.platform.log.warn('%s:%s Set value: %s (%s (%s)) confirmation differs: %s (%s)',
this.platform.log.warn('%s:%s Set value: %s: %s (tasmota: %s) not confirmed: %s: %s (tasmota: %s)',
this.accessory.context.device.name,
this.name,
value,
typeof(value),
payload,
typeof(payload),
valueToConfirm,
typeof(valueToConfirm),
hbConfirmValue,
typeof(hbConfirmValue),
confirmValue,
);
}
} catch (err) {
this.platform.log.error(err as string);
}
}
throw new this.platform.api.hap.HapStatusError(HAPStatus.OPERATION_TIMED_OUT);
}

async exec(command: TasmotaCommand, payload?: string): Promise<string> {
Expand Down Expand Up @@ -180,29 +216,6 @@ export class TasmotaCharacteristic {
});
}

setValue(origin: string, value: string | undefined) {
if (value !== undefined) {
const hbValue = this.checkHBValue(this.mapToHB(value));
if (hbValue !== undefined) {
const prevValue = this.value;
const getUpdateAlways = this.definition.get?.res?.update === true;
const updateAlways = this.definition.stat?.update === true;
const update = (hbValue !== prevValue) || updateAlways || getUpdateAlways;
if (update) {
this.service.getCharacteristic(this.platform.Characteristic[this.name]).updateValue(hbValue);
this.value = hbValue;
}
this.log('%s value%s: %s (hb: %s), prev: %s',
origin,
update ? '' : ' (not updated)',
value,
hbValue,
prevValue,
);
}
}
}

private getValueByPath(json: string, path: string): string | undefined {
let obj = Object();
try {
Expand All @@ -218,9 +231,9 @@ export class TasmotaCharacteristic {
return template.replace(/\{(.*?)\}/g, (_, key) => this.variables[key] || '');
}

private mapFromHB(value: CharacteristicValue): string | undefined {
if (Array.isArray(this.definition.mapping)) {
const mapEntry = this.definition.mapping.find(m => m.to === value);
private mapFromHB(value: CharacteristicValue, mapping?: Mapping): string | undefined {
if (Array.isArray(mapping)) {
const mapEntry = mapping.find(m => m.to === value);
if (mapEntry !== undefined) {
return mapEntry.from;
}
Expand All @@ -234,12 +247,18 @@ export class TasmotaCharacteristic {
}
}

private mapToHB(value: string): CharacteristicValue | undefined {
if (Array.isArray(this.definition.mapping)) {
const mapEntry = this.definition.mapping.find(m => m.from === value);
if (mapEntry !== undefined) {
return mapEntry.to;
private mapToHB(value: string, mapping?: Mapping): CharacteristicValue | undefined {
let mappedValue: CharacteristicValue | undefined = value;
if (mapping !== undefined) {
if (Array.isArray(mapping)) {
const mapEntry = mapping.find(m => m.from === value);
mappedValue = mapEntry?.to;
} else {
const split = value.split(mapping.separator || ',');
mappedValue = split[mapping.index];
}
}
if (mappedValue === undefined) {
return undefined;
}
switch (this.props.format) {
Expand All @@ -248,13 +267,13 @@ export class TasmotaCharacteristic {
case Formats.STRING:
case Formats.DATA:
case Formats.TLV8:
return value;
return mappedValue;
default:
return this.checkHBValue(value);
return this.checkHBValue(Number(mappedValue));
}
}

private checkHBValue(value: CharacteristicValue | undefined): CharacteristicValue | undefined {
private checkHBValue(value?: CharacteristicValue): CharacteristicValue | undefined {
if (value === undefined) {
return value;
}
Expand Down
44 changes: 31 additions & 13 deletions src/tasmotaDeviceTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const ACCESSORY_INFORMATION: TasmotaDeviceDefinition = {
default: 'Unknown',
},
FirmwareRevision: {
get: {cmd: 'STATUS 2', res: {topic: '{stat}/STATUS2', path: 'StatusFWR.Version'}},
get: {cmd: 'STATUS 2', res: {topic: '{stat}/STATUS2', path: 'StatusFWR.Version', mapping: {separator: '(', index: 0}}},
stat: {update: false},
default: 'Unknown',
},
Expand All @@ -35,32 +35,52 @@ export const DEVICE_TYPES: { [key: string] : TasmotaDeviceDefinition } = {
Brightness: {get: {cmd: 'Dimmer'}},
},
},
LIGHT_HSB: {
Lightbulb: {
On: {get: {cmd: 'POWER{idx}'}},
Hue: {
get: {cmd: 'HSBColor', res: {mapping: {index: 0}}},
set: {cmd: 'HSBColor1', res: {path: 'HSBColor'}},
},
Saturation: {
get: {cmd: 'HSBColor', res: {mapping: {index: 1}}},
set: {cmd: 'HSBColor2', res: {path: 'HSBColor'}},
},
Brightness: {
get: {cmd: 'HSBColor', res: {mapping: {index: 2}}},
set: {cmd: 'HSBColor3', res: {path: 'HSBColor'}},
},
},
},
BUTTON: {
StatelessProgrammableSwitch: {
ProgrammableSwitchEvent: {
stat: {path: 'Button{idx}.Action', update: true},
mapping: [{from: 'SINGLE', to: 0}, {from: 'DOUBLE', to: 1}, {from: 'HOLD', to: 3}],
stat: {
path: 'Button{idx}.Action',
update: true,
mapping: [{from: 'SINGLE', to: 0}, {from: 'DOUBLE', to: 1}, {from: 'HOLD', to: 3}],
},
},
},
},
CONTACT: {
ContactSensor: {
ContactSensorState: {
get: {cmd: 'STATUS 10', res: {topic: '{stat}/STATUS10', path: 'StatusSNS.Switch{idx}'}},
get: {
cmd: 'STATUS 10',
res: {topic: '{stat}/STATUS10', path: 'StatusSNS.Switch{idx}', mapping: [{from: 'ON', to: 0}, {from: 'OFF', to: 1}]},
},
stat: {path: 'Switch{idx}.Action'},
mapping: [ {from: 'ON', to: 0}, {from: 'OFF', to: 1}],
},
},
},
VALVE: {
Valve: {
Active: {
get: {cmd: 'POWER{idx}', res: {shared: true}},
mapping: [ {from: 'ON', to: 1}, {from: 'OFF', to: 0}],
get: {cmd: 'POWER{idx}', res: {shared: true, mapping: [{from: 'ON', to: 1}, {from: 'OFF', to: 0}]}},
},
InUse: {
stat: {path: 'POWER{idx}'},
mapping: [ {from: 'ON', to: 1}, {from: 'OFF', to: 0}],
stat: {path: 'POWER{idx}', mapping: [ {from: 'ON', to: 1}, {from: 'OFF', to: 0}]},
},
ValveType: {
default: 3,
Expand All @@ -73,12 +93,10 @@ export const DEVICE_TYPES: { [key: string] : TasmotaDeviceDefinition } = {
LOCK: {
LockMechanism: {
LockTargetState: {
get: {cmd: 'POWER{idx}', res: {shared: true}},
mapping: [ {from: 'ON', to: 1}, {from: 'OFF', to: 0}],
get: {cmd: 'POWER{idx}', res: {shared: true, mapping: [ {from: 'ON', to: 1}, {from: 'OFF', to: 0}]}},
},
LockCurrentState: {
stat: {path: 'POWER{idx}'},
mapping: [ {from: 'ON', to: 1}, {from: 'OFF', to: 0}],
stat: {path: 'POWER{idx}', mapping: [ {from: 'ON', to: 1}, {from: 'OFF', to: 0}]},
},
},
},
Expand Down

0 comments on commit 0496407

Please sign in to comment.