Skip to content

Commit

Permalink
Update range of numeric characteristic if an out of range value is re…
Browse files Browse the repository at this point in the history
…ceived from Zigbee2MQTT
  • Loading branch information
itavero committed Jan 4, 2025
1 parent adc5b99 commit 7afb420
Show file tree
Hide file tree
Showing 10 changed files with 50 additions and 16 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ Since version 1.0.0, we try to follow the [Semantic Versioning](https://semver.o

## [Unreleased]

### Changed

- For numeric characteristics that have a range set, the range is automatically updated if an out of range value is received from Zigbee2MQTT.

## [1.11.0-beta.7] - 2025-01-04

### Changed
Expand Down
5 changes: 4 additions & 1 deletion src/converters/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CharacteristicMonitor, MappingCharacteristicMonitor } from './monitor';
import { Characteristic, CharacteristicProps, CharacteristicValue } from 'homebridge';
import { getOrAddCharacteristic } from '../helpers';
import { SwitchActionHelper, SwitchActionMapping } from './action_helper';
import { BasicLogger } from '../logger';

export class StatelessProgrammableSwitchCreator implements ServiceCreator {
createServicesFromExposes(accessory: BasicAccessory, exposes: ExposesEntry[]): void {
Expand Down Expand Up @@ -54,13 +55,15 @@ class StatelessProgrammableSwitchHandler implements ServiceHandler {
public readonly identifier: string;
private readonly monitor: CharacteristicMonitor;
public readonly mainCharacteristics: Characteristic[] = [];
private readonly log: BasicLogger;

constructor(
accessory: BasicAccessory,
private readonly actionExpose: ExposesEntryWithEnumProperty,
mapping: SwitchActionMapping
) {
this.identifier = StatelessProgrammableSwitchHandler.generateIdentifier(actionExpose.endpoint, mapping.subType);
this.log = accessory.log;

// Create service
let subType = mapping.subType;
Expand Down Expand Up @@ -113,7 +116,7 @@ class StatelessProgrammableSwitchHandler implements ServiceHandler {
}

updateState(state: Record<string, unknown>): void {
this.monitor.callback(state);
this.monitor.callback(state, this.log);
}

static generateIdentifier(endpoint: string | undefined, mappingSubType: string | undefined) {
Expand Down
5 changes: 4 additions & 1 deletion src/converters/basic_sensors/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { CharacteristicMonitor, MappingCharacteristicMonitor } from '../monitor'
import { Characteristic, CharacteristicValue, Service } from 'homebridge';
import { getOrAddCharacteristic } from '../../helpers';
import { hap } from '../../hap';
import { BasicLogger } from '../../logger';

export type ServiceConstructor = (serviceName: string, subType: string | undefined) => Service;

export type IdentifierGenerator = (endpoint: string | undefined, accessory: BasicAccessory) => string;

export abstract class BasicSensorHandler implements ServiceHandler {
protected log: BasicLogger;
protected monitors: CharacteristicMonitor[] = [];
protected tamperExpose?: ExposesEntryWithBinaryProperty;
protected lowBatteryExpose?: ExposesEntryWithBinaryProperty;
Expand All @@ -25,6 +27,7 @@ export abstract class BasicSensorHandler implements ServiceHandler {
service: ServiceConstructor,
additionalSubType?: string | undefined
) {
this.log = accessory.log;
const endpoint = sensorExpose.endpoint;

let sub = endpoint;
Expand Down Expand Up @@ -92,6 +95,6 @@ export abstract class BasicSensorHandler implements ServiceHandler {
}

updateState(state: Record<string, unknown>): void {
this.monitors.forEach((m) => m.callback(state));
this.monitors.forEach((m) => m.callback(state, this.log));
}
}
2 changes: 1 addition & 1 deletion src/converters/battery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ class BatteryHandler implements ServiceHandler {
}

updateState(state: Record<string, unknown>): void {
this.monitors.forEach((m) => m.callback(state));
this.monitors.forEach((m) => m.callback(state, this.accessory.log));
}

static generateIdentifier(endpoint: string | undefined) {
Expand Down
2 changes: 1 addition & 1 deletion src/converters/climate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ class ThermostatHandler implements ServiceHandler {
}

updateState(state: Record<string, unknown>): void {
this.monitors.forEach((m) => m.callback(state));
this.monitors.forEach((m) => m.callback(state, this.accessory.log));
}

static generateIdentifier(endpoint: string | undefined) {
Expand Down
2 changes: 1 addition & 1 deletion src/converters/cover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ class CoverHandler implements ServiceHandler {
}

updateState(state: Record<string, unknown>): void {
this.monitors.forEach((m) => m.callback(state));
this.monitors.forEach((m) => m.callback(state, this.accessory.log));

if (this.motorStateExpose !== undefined && this.motorStateExpose.property in state) {
const latestMotorState = state[this.motorStateExpose.property] as string;
Expand Down
2 changes: 1 addition & 1 deletion src/converters/light.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ class LightHandler implements ServiceHandler {
}
}

this.monitors.forEach((m) => m.callback(state));
this.monitors.forEach((m) => m.callback(state, this.accessory.log));
}

private disableAdaptiveLightingBasedOnState(colorModeIsTemperature: boolean, state: Record<string, unknown>) {
Expand Down
2 changes: 1 addition & 1 deletion src/converters/lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ class LockHandler implements ServiceHandler {
}

updateState(state: Record<string, unknown>): void {
this.monitors.forEach((m) => m.callback(state));
this.monitors.forEach((m) => m.callback(state, this.accessory.log));
}

private handleSetState(value: CharacteristicValue, callback: CharacteristicSetCallback): void {
Expand Down
40 changes: 32 additions & 8 deletions src/converters/monitor.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Characteristic, CharacteristicValue, Service, WithUUID } from 'homebridge';
import { BasicLogger } from '../logger';

export type MqttToHomeKitValueTransformer = (value: unknown) => CharacteristicValue | undefined;

export interface CharacteristicMonitor {
callback(state: Record<string, unknown>): void;
callback(state: Record<string, unknown>, logger: BasicLogger): void;
}

abstract class BaseCharacteristicMonitor implements CharacteristicMonitor {
Expand All @@ -15,17 +16,40 @@ abstract class BaseCharacteristicMonitor implements CharacteristicMonitor {

abstract transformValueFromMqtt(value: unknown): CharacteristicValue | undefined;

callback(state: Record<string, unknown>): void {
callback(state: Record<string, unknown>, logger: BasicLogger): void {
if (this.key in state) {
let value = state[this.key];
const value = state[this.key];
if (value !== undefined) {
value = this.transformValueFromMqtt(value);
if (value !== undefined) {
this.service.updateCharacteristic(this.characteristic, value as CharacteristicValue);
const transformed_value = this.transformValueFromMqtt(value);
if (transformed_value !== undefined) {
this.updateRangeIfNeeded(transformed_value, logger);
this.service.updateCharacteristic(this.characteristic, transformed_value);
}
}
}
}

private updateRangeIfNeeded(value: CharacteristicValue, logger: BasicLogger): void {
if (typeof value !== 'number') {
// Only numeric values have a range
return;
}

const c = this.service.getCharacteristic(this.characteristic);
if (c === undefined) {
return;
}

if (c.props.minValue !== undefined && value < c.props.minValue) {
// Update min value
logger.warn(`${c.displayName} minimum value updated from ${c.props.minValue} to ${value}, due to received value.`);
c.setProps({ minValue: value });
} else if (c.props.maxValue !== undefined && value > c.props.maxValue) {
// Update max value
logger.warn(`${c.displayName} maximum value updated from ${c.props.maxValue} to ${value}, due to received value.`);
c.setProps({ maxValue: value });
}
}
}

export class NestedCharacteristicMonitor implements CharacteristicMonitor {
Expand All @@ -38,10 +62,10 @@ export class NestedCharacteristicMonitor implements CharacteristicMonitor {
}
}

callback(state: Record<string, unknown>): void {
callback(state: Record<string, unknown>, logger: BasicLogger): void {
if (this.key in state) {
const nested_state = state[this.key] as Record<string, unknown>;
this.monitors.forEach((m) => m.callback(nested_state));
this.monitors.forEach((m) => m.callback(nested_state, logger));
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/converters/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class SwitchHandler implements ServiceHandler {
}

updateState(state: Record<string, unknown>): void {
this.monitor.callback(state);
this.monitor.callback(state, this.accessory.log);
}

private handleSetOn(value: CharacteristicValue, callback: CharacteristicSetCallback): void {
Expand Down

0 comments on commit 7afb420

Please sign in to comment.