Skip to content

Zigbee2MQTT: add WOOX R7051 smart siren - #2477

Open
William-De71 wants to merge 5 commits into
GladysAssistant:masterfrom
William-De71:features/woox-siren
Open

Zigbee2MQTT: add WOOX R7051 smart siren#2477
William-De71 wants to merge 5 commits into
GladysAssistant:masterfrom
William-De71:features/woox-siren

Conversation

@William-De71

@William-De71 William-De71 commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

Pull Request check-list

To ensure your Pull Request can be accepted as fast as possible, make sure to review and check all of these items:

  • If your changes affect the code, did you write the tests?
  • Are tests passing? (npm test on both front/server)
  • Is the linter passing? (npm run eslint on both front/server)
  • x ] Did you run prettier? (npm run prettier on both front/server)
  • If you are adding a new feature/service, did you run the integration comparator? (npm run compare-translations on front)
  • Did you test this pull request in real life? With real devices? If this development is a big feature or a new service, we recommend that you provide a Docker image to the community (forum) for testing before merging.
  • If your changes modify the API (REST or Node.js), did you modify the API documentation? (Documentation is based on comments in code)
  • If you are adding a new features/services which needs explanation, did you modify the user documentation? See the GitHub repo and the website.
  • Did you add fake requests data for the demo mode (front/src/config/demo.js) so that the demo website is working without a backend? (if needed) See https://demo.gladysassistant.com.

NOTE: these things are not required to open a PR and can be done afterwards / while the PR is open.

Description of change

Add support of WOOX R7051 Smart siren
image

Summary by CodeRabbit

  • New Features

    • Siren controls: selectable modes (stop, burglar, fire, emergency, panic), volume levels (low→very high), strobe and strobe intensity, strobe duty cycle, and duration input
    • AC Connected device category added
    • Enhanced multi-level control with combined input + slider
  • Localization

    • Added translations for new siren and AC Connected labels (en/de/fr)
  • Tests

    • Added/expanded tests covering new siren mappings and composite payload handling

@coderabbitai

coderabbitai Bot commented Mar 2, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds end-to-end siren support: new frontend components and translations, new constants and icon mappings, zigbee2mqtt expose mappings for siren composite features, composite-aware MQTT ingest/emit and set/read handling, updated tests and fixtures to cover composite siren exposures and conversions.

Changes

Siren feature support (single DAG)

Layer / File(s) Summary
Constants / Types
server/utils/constants.js
Adds SIREN_MODE, VERY_HIGH in SIREN_LMH_VOLUME, AC_CONNECTED category, and new SIREN feature type keys (MODE, LEVEL, STROBE, STROBE_LEVEL, STROBE_DUTY_CYCLE, VOLUME).
Zigbee2MQTT expose descriptors
server/services/zigbee2mqtt/exposes/enumType.js, server/services/zigbee2mqtt/exposes/numericType.js, server/services/zigbee2mqtt/exposes/binaryType.js
Adds expose names and mappings for mode, level, strobe_level, strobe_duty_cycle, volume, ac_connected, and strobe (composite/binary) with read/write mapping rules and numeric bounds.
Composite type behaviour
server/services/zigbee2mqtt/exposes/compositeType.js
Conditional conversion only for color_xy; non-color composite values are passed through unchanged.
Expose matching & composite parent plumbing
server/services/zigbee2mqtt/lib/findMatchingExpose.js
findMatchingExpose now returns { expose, parent } and threads parent during recursion to identify composite parents.
MQTT message handling (ingest)
server/services/zigbee2mqtt/lib/handleMqttMessage.js
Detects composite object payloads, iterates sub-fields, converts each subValue via readValue, and emits NEW_STATE per sub-feature (with error logging).
Value read/write with parent context
server/services/zigbee2mqtt/lib/readValue.js, server/services/zigbee2mqtt/lib/setValue.js
Adapted to accept the {expose, parent} result; readValue uses result.expose; setValue builds nested payloads when parent is present and fixes mqttPayload typo.
Frontend wiring & components
front/src/components/boxs/device-in-room/DeviceRow.jsx, front/src/components/boxs/device-in-room/SupportedFeatureTypes.jsx, front/src/components/boxs/device-in-room/device-features/SirenModeDeviceFeature.jsx, front/src/components/boxs/device-in-room/device-features/SirenLevelDeviceFeature.jsx, front/src/components/boxs/device-in-room/device-features/MultiLevelWithInputDeviceFeature.jsx, front/src/components/device/SelectSirenMode.jsx, front/src/routes/scene/edit-scene/actions/DeviceSetValue.jsx
Adds new UI components for siren mode and level, a numeric-with-input slider component, maps new SIREN types to UI components in DeviceRow, adds SIREN.VOLUME to supported list, and integrates SelectSirenMode into scene editor.
Frontend icons & styles
front/src/utils/consts.js, front/src/components/boxs/device-in-room/device-features/style.css
Adds icon mappings for new siren sub-types and AC_CONNECTED; introduces CSS for numeric input, slider and stacked control layouts.
Internationalization
front/src/config/i18n/en.json, front/src/config/i18n/de.json, front/src/config/i18n/fr.json
Adds translations for siren.mode, siren.level, strobe_level, strobe_duty_cycle, volume and new ac-connected category in EN/DE/FR.
Tests & fixtures
server/test/services/zigbee2mqtt/exposes/*, server/test/services/zigbee2mqtt/lib/*, server/test/services/zigbee2mqtt/lib/payloads/*
New and extended tests for enum and composite mappings (mode/level/strobe), updates to compositeType tests, findMatchingExpose tests expecting {expose,parent}, handleMqttMessage tests for composite payloads, setValue/readValue composite tests, plus new MQTT device/event fixtures for a TS0216 siren.

Sequence Diagram

sequenceDiagram
    participant Device as Siren Device
    participant MQTT as MQTT Broker
    participant Handler as handleMqttMessage
    participant Find as findMatchingExpose
    participant Read as readValue
    participant State as State Manager
    participant UI as Frontend

    Device->>MQTT: Publish composite state (warning: {mode, level, strobe})
    MQTT->>Handler: Deliver message
    Handler->>Handler: Detect composite object value
    Handler->>Find: Find expose for parent 'warning'
    Find-->>Handler: {expose: warningExpose, parent: undefined}
    Handler->>Handler: Iterate sub-fields
    loop per sub-field
        Handler->>Find: Find expose for sub-field
        Find-->>Handler: {expose: subExpose, parent: warningExpose}
        Handler->>Read: readValue(subExpose, subValue)
        Read-->>Handler: Converted numeric/binary value
        Handler->>State: Emit NEW_STATE for subFeature
    end
    State->>UI: New state updates
    UI->>UI: Render siren mode/level/strobe controls
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • Pierre-Gilles

Poem

🐰 I hopped through code with curiosity bright,

Sirens and modes now shine in the night,
Modes, levels, strobes — nested and true,
Messages parsed into pieces anew,
A rabbit's cheer for features in sight! 🚨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Zigbee2MQTT: add WOOX R7051 smart siren' is concise and accurately describes the main addition—support for a specific Zigbee2MQTT device model. The changeset includes comprehensive backend and frontend support for siren features, and the title correctly captures the primary intent.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
server/test/services/zigbee2mqtt/lib/findMatchingExpose.test.js (1)

51-56: Rename this test for accuracy.

The title says “with parent” but the assertions validate parent is undefined. Please rename to avoid ambiguity.

✏️ Suggested rename
-  it('expose discovered with parent on cover position', () => {
+  it('expose discovered without parent on cover position', () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/test/services/zigbee2mqtt/lib/findMatchingExpose.test.js` around lines
51 - 56, The test title is misleading: the it() string "expose discovered with
parent on cover position" claims a parent but assertions check parent is
undefined; update the test description to reflect that no parent is present
(e.g., "expose discovered without parent on cover position" or "expose
discovered with no parent on cover position") in the test that calls
zigbee2MqttService.device.findMatchingExpose('0x00158d00045b2740', 'position')
so the name matches the assertions checking result.parent === undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/test/services/zigbee2mqtt/lib/findMatchingExpose.test.js`:
- Around line 59-61: The test currently dereferences sirenDevice and its
friendly_name without asserting the fixture exists; add an explicit assertion
that the fixture was found (e.g., assert(sirenDevice) or
expect(sirenDevice).toBeDefined()) before assigning to
zigbee2MqttService.device.discoveredDevices and before calling
zigbee2MqttService.device.findMatchingExpose('0x00158d00045b2741','mode');
repeat the same explicit existence check for the other fixture use around the
block referenced at lines 70-72 so failures report a clear missing-fixture error
rather than a TypeError; use the sirenDevice variable and the
zigbee2MqttService.device.discoveredDevices lookup to locate where to add the
assertion.

---

Nitpick comments:
In `@server/test/services/zigbee2mqtt/lib/findMatchingExpose.test.js`:
- Around line 51-56: The test title is misleading: the it() string "expose
discovered with parent on cover position" claims a parent but assertions check
parent is undefined; update the test description to reflect that no parent is
present (e.g., "expose discovered without parent on cover position" or "expose
discovered with no parent on cover position") in the test that calls
zigbee2MqttService.device.findMatchingExpose('0x00158d00045b2740', 'position')
so the name matches the assertions checking result.parent === undefined.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8db6b0d and 4919757.

📒 Files selected for processing (26)
  • front/src/components/boxs/device-in-room/DeviceRow.jsx
  • front/src/components/boxs/device-in-room/device-features/SirenLevelDeviceFeature.jsx
  • front/src/components/boxs/device-in-room/device-features/SirenModeDeviceFeature.jsx
  • front/src/config/i18n/de.json
  • front/src/config/i18n/en.json
  • front/src/config/i18n/fr.json
  • front/src/utils/consts.js
  • server/services/zigbee2mqtt/exposes/binaryType.js
  • server/services/zigbee2mqtt/exposes/compositeType.js
  • server/services/zigbee2mqtt/exposes/enumType.js
  • server/services/zigbee2mqtt/exposes/numericType.js
  • server/services/zigbee2mqtt/lib/findMatchingExpose.js
  • server/services/zigbee2mqtt/lib/handleMqttMessage.js
  • server/services/zigbee2mqtt/lib/readValue.js
  • server/services/zigbee2mqtt/lib/setValue.js
  • server/test/services/zigbee2mqtt/exposes/compositeType.test.js
  • server/test/services/zigbee2mqtt/exposes/warningLevelEnumType.test.js
  • server/test/services/zigbee2mqtt/exposes/warningModeEnumType.test.js
  • server/test/services/zigbee2mqtt/lib/findMatchingExpose.test.js
  • server/test/services/zigbee2mqtt/lib/getDiscoveredDevices.test.js
  • server/test/services/zigbee2mqtt/lib/handleMqttMessage.test.js
  • server/test/services/zigbee2mqtt/lib/payloads/event_device_result.json
  • server/test/services/zigbee2mqtt/lib/payloads/mqtt_devices_get.json
  • server/test/services/zigbee2mqtt/lib/readValue.test.js
  • server/test/services/zigbee2mqtt/lib/setValue.test.js
  • server/utils/constants.js

Comment on lines +59 to +61
const sirenDevice = discoveredDevices.find((d) => d.friendly_name === '0x00158d00045b2741');
zigbee2MqttService.device.discoveredDevices[sirenDevice.friendly_name] = sirenDevice;
const result = zigbee2MqttService.device.findMatchingExpose('0x00158d00045b2741', 'mode');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add an explicit fixture existence assertion before dereferencing.

If the fixture entry is missing/renamed, this will fail with a generic TypeError instead of a clear test failure.

✅ Suggested hardening
   const sirenDevice = discoveredDevices.find((d) => d.friendly_name === '0x00158d00045b2741');
+  assert.isDefined(sirenDevice, 'Expected siren fixture 0x00158d00045b2741 to exist in mqtt_devices_get.json');
   zigbee2MqttService.device.discoveredDevices[sirenDevice.friendly_name] = sirenDevice;

Also applies to: 70-72

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/test/services/zigbee2mqtt/lib/findMatchingExpose.test.js` around lines
59 - 61, The test currently dereferences sirenDevice and its friendly_name
without asserting the fixture exists; add an explicit assertion that the fixture
was found (e.g., assert(sirenDevice) or expect(sirenDevice).toBeDefined())
before assigning to zigbee2MqttService.device.discoveredDevices and before
calling
zigbee2MqttService.device.findMatchingExpose('0x00158d00045b2741','mode');
repeat the same explicit existence check for the other fixture use around the
block referenced at lines 70-72 so failures report a clear missing-fixture error
rather than a TypeError; use the sirenDevice variable and the
zigbee2MqttService.device.discoveredDevices lookup to locate where to add the
assertion.

@relativeci

relativeci Bot commented Mar 2, 2026

Copy link
Copy Markdown

#3964 Bundle Size — 11.4MiB (+0.1%).

4919757(current) vs 8db6b0d master#3902(baseline)

Warning

Bundle contains 2 duplicate packages – View duplicate packages

Bundle metrics  Change 3 changes Regression 1 regression
                 Current
#3964
     Baseline
#3902
Regression  Initial JS 6.39MiB(+0.07%) 6.38MiB
No change  Initial CSS 310.18KiB 310.18KiB
Change  Cache Invalidation 64.81% 0%
No change  Chunks 51 51
No change  Assets 179 179
Change  Modules 1645(+0.12%) 1643
No change  Duplicate Modules 21 21
No change  Duplicate Code 0.94% 0.94%
No change  Packages 136 136
No change  Duplicate Packages 2 2
Bundle size by type  Change 1 change Regression 1 regression
                 Current
#3964
     Baseline
#3902
Regression  JS 8.29MiB (+0.13%) 8.28MiB
No change  IMG 2.66MiB 2.66MiB
No change  CSS 328.03KiB 328.03KiB
No change  Fonts 93.55KiB 93.55KiB
No change  Other 18.82KiB 18.82KiB
No change  HTML 13.58KiB 13.58KiB

Bundle analysis reportBranch William-De71:features/woox-sirenProject dashboard


Generated by RelativeCIDocumentationReport issue

@codecov

codecov Bot commented Mar 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.79%. Comparing base (55c4cfc) to head (4919757).
⚠️ Report is 24 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2477      +/-   ##
==========================================
+ Coverage   98.74%   98.79%   +0.04%     
==========================================
  Files         990     1006      +16     
  Lines       17114    17514     +400     
==========================================
+ Hits        16900    17303     +403     
+ Misses        214      211       -3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
front/src/components/boxs/device-in-room/DeviceRow.jsx (1)

35-47: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

ROW_TYPE_BY_FEATURE_TYPE['mode'] is overwritten: AC mode controls will render as siren mode controls.

DEVICE_FEATURE_TYPES.AIR_CONDITIONING.MODE and DEVICE_FEATURE_TYPES.SIREN.MODE both equal the string 'mode'. In a JS object literal, the second key assignment (SirenModeDeviceFeature at line 40) silently replaces the first (AirConditioningModeDeviceFeature at line 35). Any non-read-only AC device feature with type 'mode' will now render SirenModeDeviceFeature, displaying siren alert mode options (burglar/fire/emergency…) instead of AC modes (auto/cooling/heating…).

DeviceSetValue.jsx already handles this correctly via a compound (category, type) guard. The same approach should be applied here — guard on both category and type before selecting the component.

🛠️ Proposed fix (partial — structural change needed)

The cleanest fix without a large refactor is to keep AirConditioningModeDeviceFeature in the map under the 'mode' key and add an explicit pre-check for the siren mode case, similar to DeviceSetValue.jsx:

  const elementType = ROW_TYPE_BY_FEATURE_TYPE[props.deviceFeature.type];

+  // Disambiguate types that share the same type string across categories
+  if (
+    props.deviceFeature.category === DEVICE_FEATURE_CATEGORIES.SIREN &&
+    props.deviceFeature.type === DEVICE_FEATURE_TYPES.SIREN.MODE
+  ) {
+    return createElement(SirenModeDeviceFeature, { ...props, rowName });
+  }
+
  if (!elementType) {

And remove the colliding entry from the map:

- [DEVICE_FEATURE_TYPES.SIREN.MODE]: SirenModeDeviceFeature,

You'll also need to import DEVICE_FEATURE_CATEGORIES at the top of the file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front/src/components/boxs/device-in-room/DeviceRow.jsx` around lines 35 - 47,
ROW_TYPE_BY_FEATURE_TYPE currently maps both AIR_CONDITIONING.MODE and
SIREN.MODE to the same 'mode' key so AirConditioningModeDeviceFeature is being
overwritten by SirenModeDeviceFeature; fix by importing
DEVICE_FEATURE_CATEGORIES, remove the conflicting
[DEVICE_FEATURE_TYPES.SIREN.MODE] entry from ROW_TYPE_BY_FEATURE_TYPE, keep the
AIR_CONDITIONING.MODE mapping, and add an explicit compound pre-check in
DeviceRow.jsx (similar to DeviceSetValue.jsx) that checks feature.category ===
DEVICE_FEATURE_CATEGORIES.SIREN && feature.type ===
DEVICE_FEATURE_TYPES.SIREN.MODE to render SirenModeDeviceFeature, otherwise fall
back to the ROW_TYPE_BY_FEATURE_TYPE lookup for mode.
server/utils/constants.js (1)

652-661: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

SIREN.MODE and AIR_CONDITIONING.MODE both evaluate to 'mode', causing a collision in DeviceRow.jsx.

In server/utils/constants.js, DEVICE_FEATURE_TYPES.SIREN.MODE = 'mode' and DEVICE_FEATURE_TYPES.AIR_CONDITIONING.MODE = 'mode'. In DeviceRow.jsx, the ROW_TYPE_BY_FEATURE_TYPE object uses these strings as keys (lines 35 and 40). Since both resolve to the same key, the later definition SIREN.MODE (line 40) overwrites AIR_CONDITIONING.MODE (line 35). This causes AC unit mode controls to render SirenModeDeviceFeature instead of AirConditioningModeDeviceFeature.

Additionally, SIREN.VOLUME and TELEVISION.VOLUME both evaluate to 'volume' (lines 46 and 29 in DeviceRow.jsx), causing SIREN.VOLUME to overwrite TELEVISION.VOLUME with the same consequence.

Both require fixing in DeviceRow.jsx by ensuring unique keys, or alternatively by changing the constant strings to be distinct (e.g., SIREN.MODE = 'siren-mode', SIREN.VOLUME = 'siren-volume').

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/utils/constants.js` around lines 652 - 661, The issue is key
collisions between DEVICE_FEATURE_TYPES constants (e.g., SIREN.MODE and
AIR_CONDITIONING.MODE both equal 'mode', and SIREN.VOLUME and TELEVISION.VOLUME
both equal 'volume') which makes ROW_TYPE_BY_FEATURE_TYPE in DeviceRow.jsx pick
the wrong component; fix by making the feature type keys unique or by changing
how ROW_TYPE_BY_FEATURE_TYPE indexes them — either update the constants (e.g.,
change SIREN.MODE -> 'siren-mode' and SIREN.VOLUME -> 'siren-volume') so
DEVICE_FEATURE_TYPES.SIREN.* are distinct, or modify DeviceRow.jsx to use a
composite key (e.g., `${feature.category}.${feature.type}`) when building
ROW_TYPE_BY_FEATURE_TYPE and when looking up rows so identical type names across
categories don't collide.
🧹 Nitpick comments (2)
front/src/components/device/SelectSirenMode.jsx (1)

52-61: 💤 Low value

defaultValue is ignored in controlled mode and should be removed.

When value is provided to react-select, the component operates in controlled mode and defaultValue has no effect. defaultValue={''} is misleading and should be removed.

🛠️ Proposed fix
       <Select
         class="select-device-feature"
-        defaultValue={''}
         value={selectedOption}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front/src/components/device/SelectSirenMode.jsx` around lines 52 - 61, The
Select component in SelectSirenMode.jsx is being used in controlled mode via
value={selectedOption}, so the defaultValue={''} prop is ignored and should be
removed; update the JSX for the Select (the element using value={selectedOption}
and onChange={this.handleValueChange}) by deleting the defaultValue prop to
avoid confusion and rely solely on the controlled value prop.
server/services/zigbee2mqtt/exposes/numericType.js (1)

1084-1091: volume name mapped globally to SIREN category — may misclassify other Zigbee devices.

The names map uses the Zigbee2MQTT expose name as a flat key without device-type discrimination. Any Zigbee device that exposes a numeric property named volume (e.g., a Zigbee speaker or media player) will now be classified as DEVICE_FEATURE_CATEGORIES.SIREN / DEVICE_FEATURE_TYPES.SIREN.VOLUME. This is consistent with how the existing file works (e.g., battery globally maps to BATTERY), but volume is a more widely shared expose name than most.

This is worth verifying against the existing Zigbee2MQTT device database to confirm no known non-siren devices expose a volume numeric.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/services/zigbee2mqtt/exposes/numericType.js` around lines 1084 - 1091,
The current global mapping for the expose name "volume" in the names map assigns
it to DEVICE_FEATURE_CATEGORIES.SIREN / DEVICE_FEATURE_TYPES.SIREN.VOLUME (the
"volume" entry in numericType.js), which may misclassify non-siren devices;
change this by removing the flat "volume" => SIREN mapping and instead handle
"volume" in the parsing logic where device context is available (e.g., inside
the numeric expose handler that receives the expose object or endpoint info):
only map to SIREN.VOLUME when the device model/endpoint/cluster indicates a
siren (or when expose.endpoint/name matches a known siren endpoint), otherwise
leave it unmapped or map to a generic audio/media category; update the code
paths that reference DEVICE_FEATURE_CATEGORIES.SIREN and
DEVICE_FEATURE_TYPES.SIREN.VOLUME accordingly so they only trigger when the
contextual check passes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@front/src/components/boxs/device-in-room/device-features/MultiLevelWithInputDeviceFeature.jsx`:
- Around line 28-32: handleSlider currently reads e.target.value (a string) and
passes it to setLocalValue and props.updateValueWithDebounce, causing a type
mismatch with commitInput which uses Number from clamp(); fix by coercing the
slider value to a Number (e.g., Number(...) or parseFloat(...)) inside
handleSlider before calling setLocalValue and props.updateValueWithDebounce so
both code paths (handleSlider and commitInput) always work with numeric values;
reference functions: handleSlider, commitInput, setLocalValue,
props.updateValueWithDebounce, and clamp.

In `@front/src/components/boxs/device-in-room/device-features/style.css`:
- Around line 82-87: The .numericRow CSS uses invalid values: replace
align-items: right with a valid vertical alignment (e.g., align-items: center)
and replace justify-content: right with the proper horizontal value
(justify-content: flex-end) so the flex container behaves correctly; update the
.numericRow rule accordingly.

In `@front/src/components/device/SelectSirenMode.jsx`:
- Around line 14-26: The current getOptions/componentDidMount flow defers
building deviceFeatureOptions so the initial render passes undefined to
<Select>, causing a flicker; instead compute the options once from the static
SIREN_MODE at module-level and remove state usage and lifecycle setup. Locate
getOptions, componentDidMount, deviceFeatureOptions and replace them by
exporting/defining a constant (e.g., SIREN_MODE_OPTIONS) built from
Object.keys(SIREN_MODE) with the same label/value mapping using
this.props.intl.dictionary keys if needed (or compute labels lazily via a small
helper that Select can call), then update the Select to receive that constant
(or a prop-derived array) directly and delete
getOptions/setState/componentDidMount code.

---

Outside diff comments:
In `@front/src/components/boxs/device-in-room/DeviceRow.jsx`:
- Around line 35-47: ROW_TYPE_BY_FEATURE_TYPE currently maps both
AIR_CONDITIONING.MODE and SIREN.MODE to the same 'mode' key so
AirConditioningModeDeviceFeature is being overwritten by SirenModeDeviceFeature;
fix by importing DEVICE_FEATURE_CATEGORIES, remove the conflicting
[DEVICE_FEATURE_TYPES.SIREN.MODE] entry from ROW_TYPE_BY_FEATURE_TYPE, keep the
AIR_CONDITIONING.MODE mapping, and add an explicit compound pre-check in
DeviceRow.jsx (similar to DeviceSetValue.jsx) that checks feature.category ===
DEVICE_FEATURE_CATEGORIES.SIREN && feature.type ===
DEVICE_FEATURE_TYPES.SIREN.MODE to render SirenModeDeviceFeature, otherwise fall
back to the ROW_TYPE_BY_FEATURE_TYPE lookup for mode.

In `@server/utils/constants.js`:
- Around line 652-661: The issue is key collisions between DEVICE_FEATURE_TYPES
constants (e.g., SIREN.MODE and AIR_CONDITIONING.MODE both equal 'mode', and
SIREN.VOLUME and TELEVISION.VOLUME both equal 'volume') which makes
ROW_TYPE_BY_FEATURE_TYPE in DeviceRow.jsx pick the wrong component; fix by
making the feature type keys unique or by changing how ROW_TYPE_BY_FEATURE_TYPE
indexes them — either update the constants (e.g., change SIREN.MODE ->
'siren-mode' and SIREN.VOLUME -> 'siren-volume') so DEVICE_FEATURE_TYPES.SIREN.*
are distinct, or modify DeviceRow.jsx to use a composite key (e.g.,
`${feature.category}.${feature.type}`) when building ROW_TYPE_BY_FEATURE_TYPE
and when looking up rows so identical type names across categories don't
collide.

---

Nitpick comments:
In `@front/src/components/device/SelectSirenMode.jsx`:
- Around line 52-61: The Select component in SelectSirenMode.jsx is being used
in controlled mode via value={selectedOption}, so the defaultValue={''} prop is
ignored and should be removed; update the JSX for the Select (the element using
value={selectedOption} and onChange={this.handleValueChange}) by deleting the
defaultValue prop to avoid confusion and rely solely on the controlled value
prop.

In `@server/services/zigbee2mqtt/exposes/numericType.js`:
- Around line 1084-1091: The current global mapping for the expose name "volume"
in the names map assigns it to DEVICE_FEATURE_CATEGORIES.SIREN /
DEVICE_FEATURE_TYPES.SIREN.VOLUME (the "volume" entry in numericType.js), which
may misclassify non-siren devices; change this by removing the flat "volume" =>
SIREN mapping and instead handle "volume" in the parsing logic where device
context is available (e.g., inside the numeric expose handler that receives the
expose object or endpoint info): only map to SIREN.VOLUME when the device
model/endpoint/cluster indicates a siren (or when expose.endpoint/name matches a
known siren endpoint), otherwise leave it unmapped or map to a generic
audio/media category; update the code paths that reference
DEVICE_FEATURE_CATEGORIES.SIREN and DEVICE_FEATURE_TYPES.SIREN.VOLUME
accordingly so they only trigger when the contextual check passes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2a4b9bac-1c49-4365-83d7-0fa700fc1e65

📥 Commits

Reviewing files that changed from the base of the PR and between 4919757 and 983cf53.

📒 Files selected for processing (12)
  • front/src/components/boxs/device-in-room/DeviceRow.jsx
  • front/src/components/boxs/device-in-room/SupportedFeatureTypes.jsx
  • front/src/components/boxs/device-in-room/device-features/MultiLevelWithInputDeviceFeature.jsx
  • front/src/components/boxs/device-in-room/device-features/style.css
  • front/src/components/device/SelectSirenMode.jsx
  • front/src/config/i18n/de.json
  • front/src/config/i18n/en.json
  • front/src/config/i18n/fr.json
  • front/src/routes/scene/edit-scene/actions/DeviceSetValue.jsx
  • front/src/utils/consts.js
  • server/services/zigbee2mqtt/exposes/numericType.js
  • server/utils/constants.js
✅ Files skipped from review due to trivial changes (4)
  • front/src/components/boxs/device-in-room/SupportedFeatureTypes.jsx
  • front/src/utils/consts.js
  • front/src/config/i18n/en.json
  • front/src/config/i18n/fr.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • front/src/config/i18n/de.json

Comment on lines +28 to +32
const handleSlider = e => {
const v = e.target.value;
setLocalValue(v);
props.updateValueWithDebounce(deviceFeature, v);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

handleSlider sends a string; commitInput sends a number — type inconsistency.

e.target.value on a range input is always a string, but commitInput uses clamp() which returns a Number. The backend/debounce handler likely expects a number, creating a silent type mismatch between the two code paths.

🛠️ Proposed fix
  const handleSlider = e => {
-   const v = e.target.value;
+   const v = Number(e.target.value);
    setLocalValue(v);
    props.updateValueWithDebounce(deviceFeature, v);
  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleSlider = e => {
const v = e.target.value;
setLocalValue(v);
props.updateValueWithDebounce(deviceFeature, v);
};
const handleSlider = e => {
const v = Number(e.target.value);
setLocalValue(v);
props.updateValueWithDebounce(deviceFeature, v);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@front/src/components/boxs/device-in-room/device-features/MultiLevelWithInputDeviceFeature.jsx`
around lines 28 - 32, handleSlider currently reads e.target.value (a string) and
passes it to setLocalValue and props.updateValueWithDebounce, causing a type
mismatch with commitInput which uses Number from clamp(); fix by coercing the
slider value to a Number (e.g., Number(...) or parseFloat(...)) inside
handleSlider before calling setLocalValue and props.updateValueWithDebounce so
both code paths (handleSlider and commitInput) always work with numeric values;
reference functions: handleSlider, commitInput, setLocalValue,
props.updateValueWithDebounce, and clamp.

Comment on lines +82 to +87
.numericRow {
display: flex;
align-items: right;
justify-content: right;
margin-bottom: -0.5rem;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

align-items: right is not a valid CSS property value — browsers will silently ignore it.

align-items accepts values such as flex-start, flex-end, center, baseline, stretch, etc. right is not in the spec for this property (it is partially supported for justify-content in some browsers but not align-items). If vertical centering is intended, use align-items: center.

🛠️ Proposed fix
 .numericRow {
   display: flex;
-  align-items: right;
+  align-items: center;
   justify-content: flex-end;
   margin-bottom: -0.5rem;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front/src/components/boxs/device-in-room/device-features/style.css` around
lines 82 - 87, The .numericRow CSS uses invalid values: replace align-items:
right with a valid vertical alignment (e.g., align-items: center) and replace
justify-content: right with the proper horizontal value (justify-content:
flex-end) so the flex container behaves correctly; update the .numericRow rule
accordingly.

Comment on lines +14 to +26
getOptions = () => {
const deviceFeatureOptions = Object.keys(SIREN_MODE).map(key => {
const value = SIREN_MODE[key];
return {
label: get(this.props.intl.dictionary, `deviceFeatureAction.category.siren.mode.${key.toLowerCase()}`, {
default: key.toLowerCase()
}),
value
};
});

this.setState({ deviceFeatureOptions });
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Options are undefined on first render; use a static constant instead.

deviceFeatureOptions is undefined until componentDidMount fires. On the initial render, <Select options={undefined}> shows an empty dropdown, then re-renders with options — causing a flicker. Since the options are derived entirely from the static SIREN_MODE constant, there is no reason to defer computation to componentDidMount or store them in state at all.

🛠️ Proposed fix

Compute options once as a module-level constant and remove the getOptions/componentDidMount machinery:

+const SIREN_MODE_KEYS = Object.keys(SIREN_MODE);
+
 class SelectSirenMode extends Component {
-  getOptions = () => {
-    const deviceFeatureOptions = Object.keys(SIREN_MODE).map(key => {
-      const value = SIREN_MODE[key];
-      return {
-        label: get(this.props.intl.dictionary, `deviceFeatureAction.category.siren.mode.${key.toLowerCase()}`, {
-          default: key.toLowerCase()
-        }),
-        value
-      };
-    });
-    this.setState({ deviceFeatureOptions });
-  };

-  componentDidMount() {
-    this.getOptions();
-  }

-  render(props, { deviceFeatureOptions }) {
+  render(props) {
+    const deviceFeatureOptions = SIREN_MODE_KEYS.map(key => ({
+      value: SIREN_MODE[key],
+      label: get(props.intl.dictionary, `deviceFeatureAction.category.siren.mode.${key.toLowerCase()}`, {
+        default: key.toLowerCase()
+      })
+    }));
     const selectedOption = this.getSelectedOption();
     return (
       <Select

Also applies to: 45-47

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front/src/components/device/SelectSirenMode.jsx` around lines 14 - 26, The
current getOptions/componentDidMount flow defers building deviceFeatureOptions
so the initial render passes undefined to <Select>, causing a flicker; instead
compute the options once from the static SIREN_MODE at module-level and remove
state usage and lifecycle setup. Locate getOptions, componentDidMount,
deviceFeatureOptions and replace them by exporting/defining a constant (e.g.,
SIREN_MODE_OPTIONS) built from Object.keys(SIREN_MODE) with the same label/value
mapping using this.props.intl.dictionary keys if needed (or compute labels
lazily via a small helper that Select can call), then update the Select to
receive that constant (or a prop-derived array) directly and delete
getOptions/setState/componentDidMount code.

@stale

stale Bot commented Jul 5, 2026

Copy link
Copy Markdown

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale Bot added the stale No recent activity label Jul 5, 2026
@stale stale Bot closed this Jul 13, 2026
@William-De71 William-De71 reopened this Aug 14, 2026
@stale stale Bot removed the stale No recent activity label Aug 14, 2026
@github-actions github-actions Bot added area:server Node.js server code area:front Preact front-end area:integration Services and integrations (server/services/**) labels Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:front Preact front-end area:integration Services and integrations (server/services/**) area:server Node.js server code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant