Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Refactor calibrate and precision round options #6769

Merged
merged 6 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
Collection of device converters to be used with zigbee-herdsman.

## Breaking changes
18.0.0
- After converting a message with a fromZigbee converter, `postProcessConvertedFromZigbeeMessage` should be called now (for applying calibration/precision)

17.0.0
- Various methods in `index.ts` are now async and return a `Promise`

Expand Down
93 changes: 24 additions & 69 deletions src/converters/fromZigbee.ts

Large diffs are not rendered by default.

10 changes: 3 additions & 7 deletions src/devices/bosch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,23 +696,19 @@ const fzLocal = {
bosch_twinguard_measurements: {
cluster: 'manuSpecificBosch3',
type: ['attributeReport', 'readResponse'],
options: [exposes.options.precision('temperature'), exposes.options.calibration('temperature'),
exposes.options.precision('humidity'), exposes.options.calibration('humidity'),
exposes.options.calibration('illuminance_lux', 'percentual')],
convert: (model, msg, publish, options, meta) => {
const result: KeyValue = {};
if (msg.data.hasOwnProperty('humidity')) {
result.humidity = utils.calibrateAndPrecisionRoundOptions(msg.data['humidity'] / 100.0, options, 'humidity');
result.humidity = msg.data['humidity'] / 100.0;
}
if (msg.data.hasOwnProperty('airpurity')) {
result.co2 = msg.data['airpurity'] * 10.0 + 500.0;
}
if (msg.data.hasOwnProperty('temperature')) {
result.temperature = utils.calibrateAndPrecisionRoundOptions(msg.data['temperature'] / 100.0, options, 'temperature');
result.temperature = msg.data['temperature'] / 100.0;
}
if (msg.data.hasOwnProperty('illuminance_lux')) {
result.illuminance_lux = utils.calibrateAndPrecisionRoundOptions(
msg.data['illuminance_lux'] / 2.0, options, 'illuminance_lux');
result.illuminance_lux = msg.data['illuminance_lux'] / 2.0;
}
if (msg.data.hasOwnProperty('battery')) {
result.battery = msg.data['battery'] / 2.0;
Expand Down
16 changes: 5 additions & 11 deletions src/devices/custom_devices_diy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import extend from '../lib/extend';
import * as constants from '../lib/constants';
const e = exposes.presets;
const ea = exposes.access;
import {calibrateAndPrecisionRoundOptions, getFromLookup, getKey, postfixWithEndpointName} from '../lib/utils';
import {getFromLookup, getKey, postfixWithEndpointName} from '../lib/utils';
import {light, onOff} from '../lib/modernExtend';

const switchTypesList = {
Expand Down Expand Up @@ -196,7 +196,7 @@ const fzLocal = {
convert: (model, msg, publish, options, meta) => {
if (msg.data.hasOwnProperty('measuredValue')) {
const co2 = msg.data['measuredValue'];
return {co2: calibrateAndPrecisionRoundOptions(co2, options, 'co2')};
return {co2};
}
},
} satisfies Fz.Converter,
Expand Down Expand Up @@ -308,7 +308,6 @@ const fzLocal = {
humidity2: {
cluster: 'msRelativeHumidity',
type: ['attributeReport', 'readResponse'],
options: [exposes.options.precision('humidity'), exposes.options.calibration('humidity')],
convert: (model, msg, publish, options, meta) => {
// multi-endpoint version based on the stastard onverter 'fz.humidity'
const humidity = parseFloat(msg.data['measuredValue']) / 100.0;
Expand All @@ -319,14 +318,13 @@ const fzLocal = {
if (humidity >= 0 && humidity <= 100) {
const multiEndpoint = model.meta && model.meta.hasOwnProperty('multiEndpoint') && model.meta.multiEndpoint;
const property = (multiEndpoint)? postfixWithEndpointName('humidity', msg, model, meta): 'humidity';
return {[property]: calibrateAndPrecisionRoundOptions(humidity, options, 'humidity')};
return {[property]: humidity};
}
},
} satisfies Fz.Converter,
illuminance2: {
cluster: 'msIlluminanceMeasurement',
type: ['attributeReport', 'readResponse'],
options: [exposes.options.calibration('illuminance', 'percentual'), exposes.options.calibration('illuminance_lux', 'percentual')],
convert: (model, msg, publish, options, meta) => {
// multi-endpoint version based on the stastard onverter 'fz.illuminance'
// DEPRECATED: only return lux here (change illuminance_lux -> illuminance)
Expand All @@ -335,16 +333,12 @@ const fzLocal = {
const multiEndpoint = model.meta && model.meta.hasOwnProperty('multiEndpoint') && model.meta.multiEndpoint;
const property1 = (multiEndpoint)? postfixWithEndpointName('illuminance', msg, model, meta): 'illuminance';
const property2 = (multiEndpoint)? postfixWithEndpointName('illuminance_lux', msg, model, meta): 'illuminance_lux';
return {
[property1]: calibrateAndPrecisionRoundOptions(illuminance, options, 'illuminance'),
[property2]: calibrateAndPrecisionRoundOptions(illuminanceLux, options, 'illuminance_lux'),
};
return {[property1]: illuminance, [property2]: illuminanceLux};
},
} satisfies Fz.Converter,
pressure2: {
cluster: 'msPressureMeasurement',
type: ['attributeReport', 'readResponse'],
options: [exposes.options.precision('pressure'), exposes.options.calibration('pressure')],
convert: (model, msg, publish, options, meta) => {
// multi-endpoint version based on the stastard onverter 'fz.pressure'
let pressure = 0;
Expand All @@ -356,7 +350,7 @@ const fzLocal = {
}
const multiEndpoint = model.meta && model.meta.hasOwnProperty('multiEndpoint') && model.meta.multiEndpoint;
const property = (multiEndpoint)? postfixWithEndpointName('pressure', msg, model, meta): 'pressure';
return {[property]: calibrateAndPrecisionRoundOptions(pressure, options, 'pressure')};
return {[property]: pressure};
},
} satisfies Fz.Converter,
multi_zig_sw_battery: {
Expand Down
3 changes: 1 addition & 2 deletions src/devices/develco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ const develco = {
voc: {
cluster: 'develcoSpecificAirQuality',
type: ['attributeReport', 'readResponse'],
options: [exposes.options.precision('voc'), exposes.options.calibration('voc')],
convert: (model, msg, publish, options, meta) => {
// from Sensirion_Gas_Sensors_SGP3x_TVOC_Concept.pdf
// "The mean molar mass of this mixture is 110 g/mol and hence,
Expand Down Expand Up @@ -162,7 +161,7 @@ const develco = {
} else {
airQuality = 'unknown';
}
return {[vocProperty]: utils.calibrateAndPrecisionRoundOptions(voc, options, 'voc'), [airQualityProperty]: airQuality};
return {[vocProperty]: voc, [airQualityProperty]: airQuality};
},
} satisfies Fz.Converter,
voc_battery: {
Expand Down
6 changes: 2 additions & 4 deletions src/devices/ikea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {repInterval} from '../lib/constants';
import * as utils from '../lib/utils';
import * as globalStore from '../lib/store';
import * as zigbeeHerdsman from 'zigbee-herdsman/dist';
import {calibrateAndPrecisionRoundOptions, postfixWithEndpointName, precisionRound, isObject, replaceInArray} from '../lib/utils';
import {postfixWithEndpointName, precisionRound, isObject, replaceInArray} from '../lib/utils';
import {onOff, LightArgs, light as lightDontUse} from '../lib/modernExtend';
const e = exposes.presets;
const ea = exposes.access;
Expand Down Expand Up @@ -105,7 +105,6 @@ const fzLocal = {
air_purifier: {
cluster: 'manuSpecificIkeaAirPurifier',
type: ['attributeReport', 'readResponse'],
options: [exposes.options.precision('pm25'), exposes.options.calibration('pm25')],
convert: (model, msg, publish, options, meta) => {
const state: KeyValue = {};

Expand Down Expand Up @@ -136,8 +135,7 @@ const fzLocal = {
airQuality = 'unknown';
}

// calibrate and round pm25 unless invalid
pm25 = (pm25 == 65535) ? -1 : calibrateAndPrecisionRoundOptions(pm25, options, 'pm25');
pm25 = (pm25 == 65535) ? -1 : pm25;

state[pm25Property] = pm25;
state[airQualityProperty] = airQuality;
Expand Down
6 changes: 2 additions & 4 deletions src/devices/sprut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import * as ota from '../lib/ota';
import * as constants from '../lib/constants';
import {Definition, Fz, KeyValueAny, Tz} from '../lib/types';
const e = exposes.presets;
const eo = exposes.options;
const ea = exposes.access;
import {assertString, calibrateAndPrecisionRoundOptions, getFromLookup, getOptions, toNumber} from '../lib/utils';
import {assertString, getFromLookup, getOptions, toNumber} from '../lib/utils';

const sprutCode = 0x6666;
const manufacturerOptions = {manufacturerCode: sprutCode};
Expand All @@ -22,10 +21,9 @@ const fzLocal = {
temperature: {
cluster: 'msTemperatureMeasurement',
type: ['attributeReport', 'readResponse'],
options: [eo.precision('temperature'), eo.calibration('temperature')],
convert: (model, msg, publish, options, meta) => {
const temperature = parseFloat(msg.data['measuredValue']) / 100.0;
return {temperature: calibrateAndPrecisionRoundOptions(temperature, options, 'temperature')};
return {temperature};
},
} satisfies Fz.Converter,
occupancy_level: {
Expand Down
3 changes: 1 addition & 2 deletions src/devices/tuya.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,11 +423,10 @@ const fzLocal = {
humidity10: {
cluster: 'msRelativeHumidity',
type: ['attributeReport', 'readResponse'],
options: [exposes.options.precision('humidity'), exposes.options.calibration('humidity')],
convert: (model, msg, publish, options, meta) => {
const humidity = parseFloat(msg.data['measuredValue']) / 10.0;
if (humidity >= 0 && humidity <= 100) {
return {humidity: utils.calibrateAndPrecisionRoundOptions(humidity, options, 'humidity')};
return {humidity};
}
},
} satisfies Fz.Converter,
Expand Down
7 changes: 1 addition & 6 deletions src/devices/woolley.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ const fzLocal = {
BSD29: {
cluster: '64529',
type: ['attributeReport', 'readResponse'],
options: [
exposes.options.calibration('power', 'percentual'), exposes.options.precision('power'),
exposes.options.calibration('current', 'percentual'), exposes.options.precision('current'),
exposes.options.calibration('voltage', 'percentual'), exposes.options.precision('voltage'),
],
convert: (model, msg, publish, options, meta) => {
if (utils.hasAlreadyProcessedMessage(msg, model)) return;
const lookup = [
Expand All @@ -26,7 +21,7 @@ const fzLocal = {
for (const entry of lookup) {
if (msg.data.hasOwnProperty(entry.key)) {
const value = msg.data[entry.key] / 1000;
payload[entry.name] = utils.calibrateAndPrecisionRoundOptions(value, options, entry.name);
payload[entry.name] = value;
}
}
return payload;
Expand Down
30 changes: 29 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import fromZigbee from './converters/fromZigbee';
import assert from 'assert';
import * as ota from './lib/ota';
import allDefinitions from './devices';
import { Definition, Fingerprint, Zh, OnEventData, OnEventType, Configure, Expose, Tz, OtaUpdateAvailableResult } from './lib/types';
import * as utils from './lib/utils';
import { Definition, Fingerprint, Zh, OnEventData, OnEventType, Configure, Expose, Tz, OtaUpdateAvailableResult, KeyValue } from './lib/types';
import {generateDefinition} from './lib/generateDefinition';

export {
Expand Down Expand Up @@ -190,8 +191,24 @@ function prepareDefinition(definition: Definition): Definition {

validateDefinition(definition);

// Add all the options
if (!definition.options) definition.options = [];
const optionKeys = definition.options.map((o) => o.name);

// Add calibration/precision options based on expose
for (const expose of Array.isArray(definition.exposes) ? definition.exposes : definition.exposes(null, null)) {
if (!optionKeys.includes(expose.name) && utils.isNumericExposeFeature(expose) && expose.name in utils.calibrateAndPrecisionRoundOptionsDefaultPrecision) {
// Battery voltage is not calibratable
if (expose.name === 'voltage' && expose.unit === 'mV') continue;
const type = utils.calibrateAndPrecisionRoundOptionsIsPercentual(expose.name) ? 'percentual' : 'absolute';
definition.options.push(exposes.options.calibration(expose.name, type));
if (utils.calibrateAndPrecisionRoundOptionsDefaultPrecision[expose.name] !== 0) {
definition.options.push(exposes.options.precision(expose.name));
}
optionKeys.push(expose.name);
}
}

for (const converter of [...definition.toZigbee, ...definition.fromZigbee]) {
if (converter.options) {
const options = typeof converter.options === 'function' ? converter.options(definition) : converter.options;
Expand All @@ -207,6 +224,17 @@ function prepareDefinition(definition: Definition): Definition {
return definition
}

export function postProcessConvertedFromZigbeeMessage(definition: Definition, payload: KeyValue, options: KeyValue) {
// Apply calibration/precision options
for (const [key, value] of Object.entries(payload)) {
const definitionExposes = Array.isArray(definition.exposes) ? definition.exposes : definition.exposes(null, null);
const expose = definitionExposes.find((e) => e.property === key);
if (expose?.name in utils.calibrateAndPrecisionRoundOptionsDefaultPrecision && utils.isNumber(value)) {
payload[key] = utils.calibrateAndPrecisionRoundOptions(value, options, expose.name);
}
}
}

export function addDefinition(definition: Definition) {
definition = prepareDefinition(definition)

Expand Down
Loading