diff --git a/front/src/components/boxs/device-in-room/DeviceRow.jsx b/front/src/components/boxs/device-in-room/DeviceRow.jsx index c490cf2a1e..d4e3f32b1c 100644 --- a/front/src/components/boxs/device-in-room/DeviceRow.jsx +++ b/front/src/components/boxs/device-in-room/DeviceRow.jsx @@ -12,6 +12,7 @@ import NumberDeviceFeature from './device-features/NumberDeviceFeature'; import CoverDeviceFeature from './device-features/CoverDeviceFeature'; import ThermostatDeviceFeature from './device-features/ThermostatDeviceFeature'; import AirConditioningModeDeviceFeature from './device-features/AirConditioningModeDeviceFeature'; +import LMHVolumeDeviceFeature from './device-features/LMHVolumeDeviceFeature'; const ROW_TYPE_BY_FEATURE_TYPE = { [DEVICE_FEATURE_TYPES.LIGHT.BINARY]: BinaryDeviceFeature, @@ -27,7 +28,10 @@ const ROW_TYPE_BY_FEATURE_TYPE = { [DEVICE_FEATURE_TYPES.CURTAIN.POSITION]: MultiLevelDeviceFeature, [DEVICE_FEATURE_TYPES.THERMOSTAT.TARGET_TEMPERATURE]: ThermostatDeviceFeature, [DEVICE_FEATURE_TYPES.AIR_CONDITIONING.MODE]: AirConditioningModeDeviceFeature, - [DEVICE_FEATURE_TYPES.AIR_CONDITIONING.TARGET_TEMPERATURE]: ThermostatDeviceFeature + [DEVICE_FEATURE_TYPES.AIR_CONDITIONING.TARGET_TEMPERATURE]: ThermostatDeviceFeature, + [DEVICE_FEATURE_TYPES.SIREN.LMH_VOLUME]: LMHVolumeDeviceFeature, + [DEVICE_FEATURE_TYPES.SIREN.MELODY]: NumberDeviceFeature, + [DEVICE_FEATURE_TYPES.DURATION.DECIMAL]: MultiLevelDeviceFeature }; const DeviceRow = ({ children, ...props }) => { diff --git a/front/src/components/boxs/device-in-room/SupportedFeatureTypes.jsx b/front/src/components/boxs/device-in-room/SupportedFeatureTypes.jsx index bd5c5d1705..a4e8516a8b 100644 --- a/front/src/components/boxs/device-in-room/SupportedFeatureTypes.jsx +++ b/front/src/components/boxs/device-in-room/SupportedFeatureTypes.jsx @@ -12,7 +12,10 @@ const SUPPORTED_FEATURE_TYPES = [ DEVICE_FEATURE_TYPES.SHUTTER.STATE, DEVICE_FEATURE_TYPES.THERMOSTAT.TARGET_TEMPERATURE, DEVICE_FEATURE_TYPES.AIR_CONDITIONING.MODE, - DEVICE_FEATURE_TYPES.AIR_CONDITIONING.TARGET_TEMPERATURE + DEVICE_FEATURE_TYPES.AIR_CONDITIONING.TARGET_TEMPERATURE, + DEVICE_FEATURE_TYPES.SIREN.LMH_VOLUME, + DEVICE_FEATURE_TYPES.SIREN.MELODY, + DEVICE_FEATURE_TYPES.DURATION.DECIMAL ]; export default SUPPORTED_FEATURE_TYPES; diff --git a/front/src/components/boxs/device-in-room/device-features/LMHVolumeDeviceFeature.jsx b/front/src/components/boxs/device-in-room/device-features/LMHVolumeDeviceFeature.jsx new file mode 100644 index 0000000000..36f417f62f --- /dev/null +++ b/front/src/components/boxs/device-in-room/device-features/LMHVolumeDeviceFeature.jsx @@ -0,0 +1,69 @@ +import get from 'get-value'; +import { Text } from 'preact-i18n'; +import cx from 'classnames'; + +import { DeviceFeatureCategoriesIcon } from '../../../../utils/consts'; +import { SIREN_LMH_VOLUME } from '../../../../../../server/utils/constants'; + +const LMHVolumeDeviceFeature = ({ children, ...props }) => { + const { deviceFeature } = props; + const { category, type, last_value: lastValue } = deviceFeature; + + function updateValue(value) { + props.updateValueWithDebounce(deviceFeature, value); + } + + function low() { + updateValue(SIREN_LMH_VOLUME.LOW); + } + + function medium() { + updateValue(SIREN_LMH_VOLUME.MEDIUM); + } + + function high() { + updateValue(SIREN_LMH_VOLUME.HIGH); + } + + return ( + + + + + {props.rowName} + + +
+
+ + + +
+
+ + + ); +}; + +export default LMHVolumeDeviceFeature; diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json index cbc9bbe528..7a8a9ee6c0 100644 --- a/front/src/config/i18n/de.json +++ b/front/src/config/i18n/de.json @@ -2598,6 +2598,13 @@ "cooling": "Kühlen", "heating": "Heizen" } + }, + "siren": { + "volume": { + "low": "Niedrig", + "medium": "Mittel", + "high": "Hoch" + } } } }, @@ -2792,7 +2799,9 @@ }, "siren": { "shortCategoryName": "Sirene", - "binary": "Sirene" + "binary": "Sirene", + "volume": "Lautstärke der Sirene", + "melody": "Melodie" }, "cube": { "shortCategoryName": "Würfel", diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 630912132a..08a56e0d65 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -2600,6 +2600,13 @@ "cooling": "Cool", "heating": "Heat" } + }, + "siren": { + "volume": { + "low": "Low", + "medium": "Medium", + "high": "High" + } } } }, @@ -2794,7 +2801,9 @@ }, "siren": { "shortCategoryName": "Siren", - "binary": "Siren" + "binary": "Siren", + "volume": "Siren volume", + "melody": "Melody" }, "cube": { "shortCategoryName": "Cube", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 74e11bcf48..898238441e 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -2602,6 +2602,13 @@ "cooling": "Clim.", "heating": "Chauffage" } + }, + "siren": { + "volume": { + "low": "Faible", + "medium": "Moyen", + "high": "Fort" + } } } }, @@ -2796,7 +2803,9 @@ }, "siren": { "shortCategoryName": "Sirène", - "binary": "Sirène On/Off" + "binary": "Sirène On/Off", + "volume": "Volume de la sirène", + "melody": "Mélodie" }, "cube": { "shortCategoryName": "Cube", diff --git a/front/src/utils/consts.js b/front/src/utils/consts.js index c2ff7589ba..62336bea09 100644 --- a/front/src/utils/consts.js +++ b/front/src/utils/consts.js @@ -224,7 +224,9 @@ export const DeviceFeatureCategoriesIcon = { [DEVICE_FEATURE_TYPES.SENSOR.INTEGER]: 'cloud' }, [DEVICE_FEATURE_CATEGORIES.SIREN]: { - [DEVICE_FEATURE_TYPES.SIREN.BINARY]: 'bell' + [DEVICE_FEATURE_TYPES.SIREN.BINARY]: 'bell', + [DEVICE_FEATURE_TYPES.SIREN.LMH_VOLUME]: 'volume-1', + [DEVICE_FEATURE_TYPES.SIREN.MELODY]: 'music' }, [DEVICE_FEATURE_CATEGORIES.TAMPER]: { [DEVICE_FEATURE_TYPES.SENSOR.BINARY]: 'shield' diff --git a/server/services/zigbee2mqtt/exposes/enumType.js b/server/services/zigbee2mqtt/exposes/enumType.js index 46e7280425..335cd216e9 100644 --- a/server/services/zigbee2mqtt/exposes/enumType.js +++ b/server/services/zigbee2mqtt/exposes/enumType.js @@ -3,6 +3,7 @@ const { DEVICE_FEATURE_TYPES, BUTTON_STATUS, COVER_STATE, + SIREN_LMH_VOLUME, } = require('../../../utils/constants'); const WRITE_VALUE_MAPPING = {}; @@ -80,9 +81,17 @@ addMapping('state', COVER_STATE.OPEN, 'OPEN'); addMapping('state', COVER_STATE.CLOSE, 'CLOSE'); addMapping('state', COVER_STATE.STOP, 'STOP'); +addMapping('volume', SIREN_LMH_VOLUME.LOW, 'low'); +addMapping('volume', SIREN_LMH_VOLUME.MEDIUM, 'medium'); +addMapping('volume', SIREN_LMH_VOLUME.HIGH, 'high'); + module.exports = { type: 'enum', writeValue: (expose, value) => { + if (expose.name === 'melody') { + return value; + } + const relatedValue = (WRITE_VALUE_MAPPING[expose.name] || {})[value]; if (relatedValue && expose.values.includes(relatedValue)) { @@ -92,6 +101,11 @@ module.exports = { return undefined; }, readValue: (expose, value) => { + if (expose.name === 'melody') { + const intValue = parseInt(value, 10); + return intValue; + } + const subValue = value.replace(/^(\d+_)?/, ''); return (READ_VALUE_MAPPING[expose.name] || {})[subValue]; }, @@ -117,6 +131,18 @@ module.exports = { }, }, }, + volume: { + feature: { + category: DEVICE_FEATURE_CATEGORIES.SIREN, + type: DEVICE_FEATURE_TYPES.SIREN.LMH_VOLUME, + }, + }, + melody: { + feature: { + category: DEVICE_FEATURE_CATEGORIES.SIREN, + type: DEVICE_FEATURE_TYPES.SIREN.MELODY, + }, + }, }, getFeatureIndexes: (values = []) => { const indexes = values diff --git a/server/services/zigbee2mqtt/exposes/numericType.js b/server/services/zigbee2mqtt/exposes/numericType.js index cdc9affaad..05c3f296e6 100644 --- a/server/services/zigbee2mqtt/exposes/numericType.js +++ b/server/services/zigbee2mqtt/exposes/numericType.js @@ -33,6 +33,15 @@ module.exports = { max: 100, }, }, + battpercentage: { + feature: { + category: DEVICE_FEATURE_CATEGORIES.BATTERY, + type: DEVICE_FEATURE_TYPES.SENSOR.INTEGER, + unit: DEVICE_FEATURE_UNITS.PERCENT, + min: 0, + max: 100, + }, + }, brightness: { types: { light: { diff --git a/server/test/services/zigbee2mqtt/exposes/melodyEnumType.test.js b/server/test/services/zigbee2mqtt/exposes/melodyEnumType.test.js new file mode 100644 index 0000000000..5b770c0878 --- /dev/null +++ b/server/test/services/zigbee2mqtt/exposes/melodyEnumType.test.js @@ -0,0 +1,19 @@ +const { assert } = require('chai'); + +const enumType = require('../../../../services/zigbee2mqtt/exposes/enumType'); + +describe('zigbee2mqtt melody enumType', () => { + const expose = { + name: 'melody', + }; + + it('should write value', () => { + const result = enumType.writeValue(expose, 1); + assert.equal(result, 1); + }); + + it(`should read value`, () => { + const result = enumType.readValue(expose, '1'); + assert.equal(result, 1); + }); +}); diff --git a/server/test/services/zigbee2mqtt/exposes/volumeEnumType.test.js b/server/test/services/zigbee2mqtt/exposes/volumeEnumType.test.js new file mode 100644 index 0000000000..c642c73202 --- /dev/null +++ b/server/test/services/zigbee2mqtt/exposes/volumeEnumType.test.js @@ -0,0 +1,47 @@ +const { assert } = require('chai'); + +const enumType = require('../../../../services/zigbee2mqtt/exposes/enumType'); +const { SIREN_LMH_VOLUME } = require('../../../../utils/constants'); + +describe('zigbee2mqtt volume enumType', () => { + const expose = { + name: 'volume', + values: ['low', 'medium', 'high'], + }; + + [ + { enumValue: 'low', intValue: SIREN_LMH_VOLUME.LOW }, + { enumValue: 'medium', intValue: SIREN_LMH_VOLUME.MEDIUM }, + { enumValue: 'high', intValue: SIREN_LMH_VOLUME.HIGH }, + ].forEach((mapping) => { + const { enumValue, intValue } = mapping; + + it(`should write ${enumValue} value as ${intValue} value`, () => { + const result = enumType.writeValue(expose, intValue); + assert.equal(result, enumValue); + }); + + it(`should read ${intValue} value as ${enumValue}`, () => { + const result = enumType.readValue(expose, enumValue); + assert.equal(result, intValue); + }); + }); + + it('should write undefined value on missing enum', () => { + const missingEnumExpose = { + values: ['low', 'medium'], + }; + const result = enumType.writeValue(missingEnumExpose, SIREN_LMH_VOLUME.HIGH); + assert.equal(result, undefined); + }); + + it('should write undefined value', () => { + const result = enumType.writeValue(expose, 7); + assert.equal(result, undefined); + }); + + it('should read enum value', () => { + const result = enumType.readValue(expose, 'unknown'); + assert.equal(result, undefined); + }); +}); diff --git a/server/utils/constants.js b/server/utils/constants.js index 908a28f72f..b6357880d7 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -63,6 +63,12 @@ const COVER_STATE = { CLOSE: -1, }; +const SIREN_LMH_VOLUME = { + LOW: 0, + MEDIUM: 1, + HIGH: 2, +}; + const AC_MODE = { AUTO: 0, COOLING: 1, @@ -498,6 +504,8 @@ const DEVICE_FEATURE_TYPES = { }, SIREN: { BINARY: 'binary', + LMH_VOLUME: 'volume', + MELODY: 'melody', }, CHILD_LOCK: { BINARY: 'binary', @@ -1111,6 +1119,7 @@ const ALARM_MODES_LIST = createList(ALARM_MODES); module.exports.STATE = STATE; module.exports.BUTTON_STATUS = BUTTON_STATUS; module.exports.COVER_STATE = COVER_STATE; +module.exports.SIREN_LMH_VOLUME = SIREN_LMH_VOLUME; module.exports.AC_MODE = AC_MODE; module.exports.EVENTS = EVENTS; module.exports.LIFE_EVENTS = LIFE_EVENTS;