Skip to content

Tuya - add mapping core for local/cloud device handling - PR3#2466

Open
Terdious wants to merge 67 commits intoGladysAssistant:masterfrom
Terdious:tuya-local-mapping-core
Open

Tuya - add mapping core for local/cloud device handling - PR3#2466
Terdious wants to merge 67 commits intoGladysAssistant:masterfrom
Terdious:tuya-local-mapping-core

Conversation

@Terdious
Copy link
Copy Markdown
Contributor

@Terdious Terdious commented Feb 27, 2026

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)
  • 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

This PR introduces a mapping core for Tuya devices to make local/cloud handling more consistent and future device integrations easier.

Summary

  • Add the Tuya mapping core for cloud/local device support.
  • Introduce a reusable mapping architecture so new Tuya device types can be integrated more easily.
  • Add the first complete mapped device family support with Smart Socket, both in cloud and local mode.

Details

  • Add a central mapping registry for:
    • cloud mappings
    • local mappings
    • ignored cloud codes
    • ignored local DPS
    • device type detection
  • Add robust device-type detection based on:
    • category
    • product ID
    • model/product names
    • payload codes from specifications, thing model, properties and features
  • Add mapping-driven conversion improvements for:
    • convertDevice
    • convertFeature
    • tuya.deviceMapping
    • tuya.localMapping
  • Add Smart Socket support for:
    • cloud polling/state handling
    • local DPS mapping
  • Harden polling and mapping behavior:
    • mixed payload-source detection
    • normalized code matching
    • graceful cloud/local fallback
    • reader isolation and regression coverage
  • Expand tests to keep the Tuya mapping core fully covered and maintainable.

Scope

  • Translate

    • +2 lines / -5 lines
  • Front

    • +7 lines / -10 lines
  • Server

    • +924 lines / -170 lines
  • Server Tests

    • +1171 lines / -27 lines

Summary by CodeRabbit

  • New Features

    • Local UDP discovery and local polling for Tuya devices; cloud/local mode toggle, manual cloud disconnect, and explicit Save → Connect flow.
    • Real-time connection status and disconnect controls with improved reconnect handling.
  • Improvements

    • Updated setup copy and field labels (Client ID/Client Secret, App UID, App Username) with validation and user-friendly error messages.
    • Device panel shows identifiers, IP/protocol, product info, local-poll controls/progress, success/error indicators and clearer "unknown" value labels.

Terdious and others added 30 commits February 11, 2026 15:58
…er models

- Added support for air conditioning devices with new mappings and DPS configurations.
- Introduced local polling for Tuya devices to improve responsiveness.
- Enhanced device conversion logic to include additional parameters such as cloud IP and local override.
- Updated feature conversion to utilize advanced DPS mappings for air conditioning.
- Implemented new models for air conditioning and power meter, including specific feature mappings.
- Improved error handling and logging for local polling and device value setting.
- Added unit tests for new feature mappings and conversion logic.
… champ d'erreur dans le payload de l'événement WebSocket
…s et ajouter des tests pour la gestion des appareils locaux
…eurs de port et mise à jour des traductions
…re des liens vers la documentation et les options de connexion
…réation de rapports GitHub pour les appareils Tuya
…l disconnect features

- Added new translations for connection status messages in German, English, and French.
- Implemented API endpoints to get Tuya connection status and to manually disconnect from the Tuya cloud.
- Updated the Tuya service to handle automatic reconnection logic and manual disconnect state.
- Enhanced the SetupTab component to reflect connection status and provide a disconnect button.
- Added tests for the new functionality, including status retrieval and manual disconnect.
- Implemented device ranking and sorting in DiscoverTab for better user experience.
- Added loading indicators and improved UI feedback during device scanning.
- Refactored local polling logic to update discovered devices with local information.
- Introduced utility functions for managing device parameters, including upserting and normalizing values.
- Enhanced local scan response handling to merge existing device parameters.
- Updated tests to cover new functionality and ensure reliability of device management.
…age, ajouter des tests pour la reconnexion automatique et la découverte des appareils
… des paramètres dans le code de configuration Tuya
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
server/services/tuya/lib/tuya.poll.js (2)

102-154: ⚠️ Potential issue | 🟠 Major

Guard feature iteration against non-array payloads.

Line 126 calls deviceFeatures.forEach without verifying that deviceFeatures is an array. While line 104 checks for Array.isArray to set summary.polled, a non-array payload will still throw at line 126. This was flagged in a previous review.

🛠️ Proposed defensive fix
 const pollCloudFeatures = async function pollCloudFeatures(deviceFeatures, topic) {
   const summary = {
     polled: Array.isArray(deviceFeatures) ? deviceFeatures.length : 0,
     handled: 0,
     changed: 0,
     missing: 0,
     skipped: 0,
   };

+  if (!Array.isArray(deviceFeatures) || deviceFeatures.length === 0) {
+    return summary;
+  }
+
   if (!this.connector || typeof this.connector.request !== 'function') {
     logger.warn(`[Tuya][poll][cloud] connector unavailable for device=${topic}`);
     return summary;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/services/tuya/lib/tuya.poll.js` around lines 102 - 154, The
pollCloudFeatures function currently calls deviceFeatures.forEach and can throw
if deviceFeatures is not an array; update pollCloudFeatures to guard the
iteration by either returning early when deviceFeatures is not an array
(consistent with how summary.polled is computed) or by iterating over
Array.isArray(deviceFeatures) ? deviceFeatures : [] so the loop never runs on
non-array payloads; reference the pollCloudFeatures function and the
deviceFeatures usage to locate where to add the guard.

225-255: ⚠️ Potential issue | 🟠 Major

Guard device.features iteration against non-array payloads.

device.features.forEach at line 225 assumes device.features is always an array. A malformed device object could throw and break polling for that device. This was flagged in a previous review alongside the pollCloudFeatures guard.

🛠️ Proposed defensive fix
+      const deviceFeatures = Array.isArray(device.features) ? device.features : [];
       const pendingCloudFeatures = [];

-      device.features.forEach((deviceFeature) => {
+      deviceFeatures.forEach((deviceFeature) => {
         const code = getFeatureCode(deviceFeature);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/services/tuya/lib/tuya.poll.js` around lines 225 - 255, The loop
assumes device.features is always an array and can throw for malformed devices;
before calling device.features.forEach in the polling routine, add a guard using
Array.isArray(device.features) (or coerce to an empty array) so the code
iterates only when features is an array; locate the snippet where
device.features.forEach is used (alongside helpers like getFeatureCode,
getLocalDpsFromCode, getFeatureReader, emitFeatureState, getCurrentFeatureState)
and either early-return for bad payloads or replace the forEach with iteration
over a safe const features = Array.isArray(device.features) ? device.features :
[].
server/services/tuya/lib/mappings/index.js (1)

115-131: ⚠️ Potential issue | 🟠 Major

Include specifications.properties in code extraction.

This function still only extracts codes from specifications.functions and specifications.status, missing codes that may appear in specifications.properties. This was flagged in a previous review and remains unaddressed.

🔧 Proposed fix
 const extractCodesFromSpecifications = (specifications) => {
   const codes = new Set();
   if (!specifications || typeof specifications !== 'object') {
     return codes;
   }
   const functions = Array.isArray(specifications.functions) ? specifications.functions : [];
   const status = Array.isArray(specifications.status) ? specifications.status : [];
+  const properties = Array.isArray(specifications.properties) ? specifications.properties : [];

-  [...functions, ...status].forEach((item) => {
+  [...functions, ...status, ...properties].forEach((item) => {
     if (!item || !item.code) {
       return;
     }
     codes.add(String(item.code).toLowerCase());
   });

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

In `@server/services/tuya/lib/mappings/index.js` around lines 115 - 131, The
extractCodesFromSpecifications function currently only reads
specifications.functions and specifications.status; update it to also read
specifications.properties (e.g., const properties =
Array.isArray(specifications.properties) ? specifications.properties : []) and
include those items when iterating so codes from properties are captured
(convert item.code to string, toLowerCase(), and add to the codes Set, same as
for functions/status) while keeping the existing null/undefined checks.
🧹 Nitpick comments (1)
server/services/tuya/lib/mappings/index.js (1)

57-79: Verify that empty REQUIRED_CODES is intentionally permissive.

At line 73, hasRequiredCode is true when requiredCodes.size === 0, meaning device types without required codes will match based solely on modelName keywords. This may cause unintended matches if a future device type definition omits REQUIRED_CODES.

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

In `@server/services/tuya/lib/mappings/index.js` around lines 57 - 79, The
matchDeviceType function currently treats an empty REQUIRED_CODES set as
permissive (hasRequiredCode true), causing matches based solely on modelName;
change this so empty REQUIRED_CODES does not satisfy the requirement—update the
logic in matchDeviceType (use requiredCodes and hasRequiredCode) to require
requiredCodes.size > 0 and at least one code match (e.g., hasRequiredCode =
requiredCodes.size > 0 && Array.from(requiredCodes).some(code =>
codes.has(code))); alternatively, if permissive empty behavior is desired, make
it explicit by introducing a flag (e.g., ALLOW_EMPTY_REQUIRED_CODES) on the
typeDefinition and check that flag when computing hasRequiredCode.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@server/services/tuya/lib/mappings/index.js`:
- Around line 115-131: The extractCodesFromSpecifications function currently
only reads specifications.functions and specifications.status; update it to also
read specifications.properties (e.g., const properties =
Array.isArray(specifications.properties) ? specifications.properties : []) and
include those items when iterating so codes from properties are captured
(convert item.code to string, toLowerCase(), and add to the codes Set, same as
for functions/status) while keeping the existing null/undefined checks.

In `@server/services/tuya/lib/tuya.poll.js`:
- Around line 102-154: The pollCloudFeatures function currently calls
deviceFeatures.forEach and can throw if deviceFeatures is not an array; update
pollCloudFeatures to guard the iteration by either returning early when
deviceFeatures is not an array (consistent with how summary.polled is computed)
or by iterating over Array.isArray(deviceFeatures) ? deviceFeatures : [] so the
loop never runs on non-array payloads; reference the pollCloudFeatures function
and the deviceFeatures usage to locate where to add the guard.
- Around line 225-255: The loop assumes device.features is always an array and
can throw for malformed devices; before calling device.features.forEach in the
polling routine, add a guard using Array.isArray(device.features) (or coerce to
an empty array) so the code iterates only when features is an array; locate the
snippet where device.features.forEach is used (alongside helpers like
getFeatureCode, getLocalDpsFromCode, getFeatureReader, emitFeatureState,
getCurrentFeatureState) and either early-return for bad payloads or replace the
forEach with iteration over a safe const features =
Array.isArray(device.features) ? device.features : [].

---

Nitpick comments:
In `@server/services/tuya/lib/mappings/index.js`:
- Around line 57-79: The matchDeviceType function currently treats an empty
REQUIRED_CODES set as permissive (hasRequiredCode true), causing matches based
solely on modelName; change this so empty REQUIRED_CODES does not satisfy the
requirement—update the logic in matchDeviceType (use requiredCodes and
hasRequiredCode) to require requiredCodes.size > 0 and at least one code match
(e.g., hasRequiredCode = requiredCodes.size > 0 &&
Array.from(requiredCodes).some(code => codes.has(code))); alternatively, if
permissive empty behavior is desired, make it explicit by introducing a flag
(e.g., ALLOW_EMPTY_REQUIRED_CODES) on the typeDefinition and check that flag
when computing hasRequiredCode.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a9f835 and f82cc73.

📒 Files selected for processing (2)
  • server/services/tuya/lib/mappings/index.js
  • server/services/tuya/lib/tuya.poll.js

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.85%. Comparing base (8db6b0d) to head (73ac5f0).
⚠️ Report is 18 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2466      +/-   ##
==========================================
+ Coverage   98.79%   98.85%   +0.05%     
==========================================
  Files        1006     1019      +13     
  Lines       17478    18387     +909     
==========================================
+ Hits        17268    18177     +909     
  Misses        210      210              

☔ 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
server/services/tuya/lib/tuya.poll.js (1)

99-106: ⚠️ Potential issue | 🟠 Major

Guard feature iteration in both cloud and local paths.

Line 123 and Line 222 assume arrays and call .forEach directly. If device.features is malformed, polling throws and cloud fallback on Line 281 can fail for the same reason.

🛠️ Proposed defensive fix
 const pollCloudFeatures = async function pollCloudFeatures(deviceFeatures, topic) {
   const summary = {
     polled: Array.isArray(deviceFeatures) ? deviceFeatures.length : 0,
     handled: 0,
     changed: 0,
     missing: 0,
     skipped: 0,
   };
+
+  if (!Array.isArray(deviceFeatures) || deviceFeatures.length === 0) {
+    return summary;
+  }

   if (!this.connector || typeof this.connector.request !== 'function') {
     logger.warn(`[Tuya][poll][cloud] connector unavailable for device=${topic}`);
     return summary;
   }
@@
 async function poll(device) {
@@
   const params = device.params || [];
+  const deviceFeatures = Array.isArray(device.features) ? device.features : [];
@@
-        device.features.forEach((deviceFeature) => {
+        deviceFeatures.forEach((deviceFeature) => {
@@
-  cloudSummary = await pollCloudFeatures.call(this, device.features, topic);
+  cloudSummary = await pollCloudFeatures.call(this, deviceFeatures, topic);

Also applies to: 123-123, 222-223, 281-281

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

In `@server/services/tuya/lib/tuya.poll.js` around lines 99 - 106, Guard array
iterations of device features in pollCloudFeatures and its local/cloud fallback
paths: check Array.isArray(device.features) (or deviceFeatures passed into
pollCloudFeatures) before calling .forEach or otherwise iterate; if not an
array, treat as empty (increment summary.missing/skipped as appropriate) or
return early. Update the loops referenced around the uses of device.features in
the cloud path and local fallback so they never call .forEach on a non-array and
ensure summary counters (polled/handled/changed/missing/skipped) are updated
consistently when features are malformed.
🧹 Nitpick comments (3)
server/test/services/tuya/lib/tuya.localScan.test.js (2)

311-312: Add explicit assertion for ipParam existence.

If ipParam is undefined, accessing .value will throw a TypeError with a confusing message rather than a clear test failure. Adding an explicit existence check improves test diagnostics.

💡 Proposed fix
     const ipParam = response.devices[0].params.find((param) => param.name === 'IP_ADDRESS');
+    expect(ipParam).to.exist;
     expect(ipParam.value).to.equal('1.1.1.1');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/test/services/tuya/lib/tuya.localScan.test.js` around lines 311 - 312,
Add an explicit assertion that the found parameter exists before accessing its
value: after computing ipParam (from response.devices[0].params.find(...)) add a
check like expect(ipParam).to.exist or expect(ipParam).to.be.ok to fail with a
clear message if the param is missing, then keep the existing
expect(ipParam.value).to.equal('1.1.1.1'); so the test reports a clear failure
when the IP param isn't present.

253-293: Test exercises code path but lacks assertions on logging behavior.

The test description states "should log listening address when available" but there's no assertion verifying that logging actually occurs. Consider adding a spy on the logger to verify the expected log message, or rename the test to clarify it's for coverage purposes.

💡 Proposed enhancement to verify logging
 it('should log listening address when available', async () => {
+  const loggerSpy = sinon.spy();
   const sockets = [];
   const dgramStub = {
     createSocket: () => {
       // ... existing socket setup ...
     },
   };

   // ... existing parser stub ...

   const { localScan } = proxyquire('../../../../services/tuya/lib/tuya.localScan', {
     dgram: dgramStub,
     '@demirdeniz/tuyapi-newgen/lib/message-parser': { MessageParser: MessageParserStub },
     '@demirdeniz/tuyapi-newgen/lib/config': { UDP_KEY: 'key' },
+    '../../../utils/logger': { debug: loggerSpy },
   });

   const clock = sinon.useFakeTimers();
   const promise = localScan(1);
   await clock.tickAsync(1100);
   await promise;
   clock.restore();
+
+  expect(loggerSpy.called).to.be.true;
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/test/services/tuya/lib/tuya.localScan.test.js` around lines 253 - 293,
The test exercises localScan but doesn't assert logging; update the test to
spy/stub the logger used by the module (e.g., the processLogger or logger
instance referenced inside localScan) before calling localScan, then assert that
the logger method (info/debug) was called with a message containing the
listening address and port '0.0.0.0:6666' (or a substring like '0.0.0.0' and
'6666'); alternatively, if you don't want to assert logging, rename the test to
reflect it's only exercising the code path for coverage. Ensure you reference
the localScan import from '../../../../services/tuya/lib/tuya.localScan' and
attach the spy before invoking localScan so the assertion can observe the log
call.
server/test/services/tuya/lib/tuya.poll.test.js (1)

341-581: Add one regression test for malformed features payloads.

Given the new branch-coverage block, a non-array features case would lock in graceful degradation and prevent runtime regressions.

🧪 Suggested test addition
 describe('TuyaHandler.poll additional branch coverage', () => {
+  it('should not throw when features payload is not an array', async () => {
+    const request = sinon.stub().resolves({ result: [{ code: 'switch_1', value: true }] });
+    const emit = sinon.stub();
+    const { poll } = proxyquire('../../../../services/tuya/lib/tuya.poll', {});
+
+    await poll.call(
+      {
+        connector: { request },
+        gladys: { event: { emit } },
+      },
+      {
+        external_id: 'tuya:device',
+        params: [{ name: 'LOCAL_OVERRIDE', value: false }],
+        features: null,
+      },
+    );
+
+    expect(emit.called).to.equal(false);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/test/services/tuya/lib/tuya.poll.test.js` around lines 341 - 581, Add
a regression test to the existing "TuyaHandler.poll additional branch coverage"
suite that calls the poll function (poll) with a device whose features property
is malformed (e.g., null or an object instead of an array) and asserts that poll
does not throw, logs a warning via the injected logger (logger.warn called),
does not emit events (gladys.event.emit not called), and does not crash the
connector.request; stub connector.request and gladys.event.emit as in the other
tests and place the test alongside the other it(...) blocks so malformed
features gracefully degrade.
🤖 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/services/tuya/lib/tuya.poll.js`:
- Around line 118-121: The code currently assumes response.result is an array
and calls forEach, which can throw if response.result is truthy but not an
array; update the parsing in tuya.poll.js to explicitly guard with
Array.isArray(response.result) (or coerce using Array.isArray ? response.result
: []) before iterating so values remains empty/safe when the payload shape is
unexpected; locate the block that builds values (the values = {} declaration and
the forEach over response.result) and replace the direct forEach with an
iteration over a validated array to prevent runtime errors.

---

Duplicate comments:
In `@server/services/tuya/lib/tuya.poll.js`:
- Around line 99-106: Guard array iterations of device features in
pollCloudFeatures and its local/cloud fallback paths: check
Array.isArray(device.features) (or deviceFeatures passed into pollCloudFeatures)
before calling .forEach or otherwise iterate; if not an array, treat as empty
(increment summary.missing/skipped as appropriate) or return early. Update the
loops referenced around the uses of device.features in the cloud path and local
fallback so they never call .forEach on a non-array and ensure summary counters
(polled/handled/changed/missing/skipped) are updated consistently when features
are malformed.

---

Nitpick comments:
In `@server/test/services/tuya/lib/tuya.localScan.test.js`:
- Around line 311-312: Add an explicit assertion that the found parameter exists
before accessing its value: after computing ipParam (from
response.devices[0].params.find(...)) add a check like expect(ipParam).to.exist
or expect(ipParam).to.be.ok to fail with a clear message if the param is
missing, then keep the existing expect(ipParam.value).to.equal('1.1.1.1'); so
the test reports a clear failure when the IP param isn't present.
- Around line 253-293: The test exercises localScan but doesn't assert logging;
update the test to spy/stub the logger used by the module (e.g., the
processLogger or logger instance referenced inside localScan) before calling
localScan, then assert that the logger method (info/debug) was called with a
message containing the listening address and port '0.0.0.0:6666' (or a substring
like '0.0.0.0' and '6666'); alternatively, if you don't want to assert logging,
rename the test to reflect it's only exercising the code path for coverage.
Ensure you reference the localScan import from
'../../../../services/tuya/lib/tuya.localScan' and attach the spy before
invoking localScan so the assertion can observe the log call.

In `@server/test/services/tuya/lib/tuya.poll.test.js`:
- Around line 341-581: Add a regression test to the existing "TuyaHandler.poll
additional branch coverage" suite that calls the poll function (poll) with a
device whose features property is malformed (e.g., null or an object instead of
an array) and asserts that poll does not throw, logs a warning via the injected
logger (logger.warn called), does not emit events (gladys.event.emit not
called), and does not crash the connector.request; stub connector.request and
gladys.event.emit as in the other tests and place the test alongside the other
it(...) blocks so malformed features gracefully degrade.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f82cc73 and 27152bc.

📒 Files selected for processing (6)
  • server/services/tuya/lib/mappings/index.js
  • server/services/tuya/lib/tuya.poll.js
  • server/test/services/tuya/lib/device/feature/tuya.convertFeature.test.js
  • server/test/services/tuya/lib/mappings/index.test.js
  • server/test/services/tuya/lib/tuya.localScan.test.js
  • server/test/services/tuya/lib/tuya.poll.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/test/services/tuya/lib/device/feature/tuya.convertFeature.test.js
  • server/services/tuya/lib/mappings/index.js

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx (1)

488-488: ⚠️ Potential issue | 🟡 Minor

Use autoComplete casing in JSX (Line 488).

autocomplete should be autoComplete so the prop is correctly handled.

Suggested change
-                        autocomplete="off"
+                        autoComplete="off"
#!/bin/bash
# Read-only verification: find lowercase JSX autocomplete usage
rg -n -C2 '\bautocomplete=' --type js --type jsx
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx` at line 488,
In SetupTab.jsx update the JSX attribute name from the lowercase HTML form
attribute "autocomplete" to React/JSX camelCase "autoComplete" on the input
element inside the SetupTab component (the element currently rendering
autocomplete="off"); change the prop there to autoComplete="off" so React
recognizes and applies it correctly.
🧹 Nitpick comments (2)
front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx (2)

29-34: Move side effects/subscriptions out of componentWillMount.

Using network calls and websocket listeners here is fragile with modern React/Preact lifecycle behavior; use componentDidMount for these side effects.

Suggested change
-  componentWillMount() {
+  componentDidMount() {
     this.getTuyaConfiguration();
     this.getTuyaStatus();
     this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.TUYA.STATUS, this.updateConnectionStatus);
     this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.TUYA.ERROR, this.displayConnectionError);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx` around lines
29 - 34, The lifecycle side-effects (network calls and websocket subscriptions)
are placed in componentWillMount; move them to componentDidMount: call
this.getTuyaConfiguration() and this.getTuyaStatus() and register the listeners
via
this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.TUYA.STATUS,
this.updateConnectionStatus) and addListener(WEBSOCKET_MESSAGE_TYPES.TUYA.ERROR,
this.displayConnectionError) inside componentDidMount, and remove or leave
componentWillMount empty; ensure any corresponding cleanup (removing those
listeners) is done in componentWillUnmount.

492-500: Make the secret-toggle control keyboard-accessible.

A clickable <span> is not keyboard-friendly. Use a <button type="button"> with aria-label/aria-pressed.

Suggested change
-                    <span class="input-icon-addon cursor-pointer" onClick={this.toggleClientSecret}>
+                    <button
+                      type="button"
+                      class="input-icon-addon cursor-pointer"
+                      onClick={this.toggleClientSecret}
+                      aria-pressed={state.showClientSecret}
+                      aria-label="Toggle secret visibility"
+                    >
                       <i
                         class={cx('fe', {
                           'fe-eye': !state.showClientSecret,
                           'fe-eye-off': state.showClientSecret
                         })}
                       />
-                    </span>
+                    </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx` around lines
492 - 500, The secret-toggle is a clickable <span> and must be
keyboard-accessible: replace the <span class="input-icon-addon cursor-pointer"
onClick={this.toggleClientSecret}> with a <button type="button"> that preserves
the same classes and onClick handler (toggleClientSecret) and add accessibility
attributes such as aria-label (e.g., "Toggle client secret visibility") and
aria-pressed={state.showClientSecret}; ensure the inner <i> still uses the
cx(...) logic for icon classes and the element remains focusable and styled like
the original control.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx`:
- Around line 133-150: In the SetupTab component's save handler, you're calling
this.setState(...) then immediately reading this.state values (tuyaEndpoint,
tuyaAccessKey, tuyaSecretKey, tuyaAppAccountId, tuyaAppUsername), which can race
with React's async setState; capture the form values into local constants before
calling setState (e.g., const tuyaEndpoint = (this.state.tuyaEndpoint ||
'').trim(); etc.), then call this.setState(...) and use those local constants
for the save logic instead of re-reading this.state.

---

Duplicate comments:
In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx`:
- Line 488: In SetupTab.jsx update the JSX attribute name from the lowercase
HTML form attribute "autocomplete" to React/JSX camelCase "autoComplete" on the
input element inside the SetupTab component (the element currently rendering
autocomplete="off"); change the prop there to autoComplete="off" so React
recognizes and applies it correctly.

---

Nitpick comments:
In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx`:
- Around line 29-34: The lifecycle side-effects (network calls and websocket
subscriptions) are placed in componentWillMount; move them to componentDidMount:
call this.getTuyaConfiguration() and this.getTuyaStatus() and register the
listeners via
this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.TUYA.STATUS,
this.updateConnectionStatus) and addListener(WEBSOCKET_MESSAGE_TYPES.TUYA.ERROR,
this.displayConnectionError) inside componentDidMount, and remove or leave
componentWillMount empty; ensure any corresponding cleanup (removing those
listeners) is done in componentWillUnmount.
- Around line 492-500: The secret-toggle is a clickable <span> and must be
keyboard-accessible: replace the <span class="input-icon-addon cursor-pointer"
onClick={this.toggleClientSecret}> with a <button type="button"> that preserves
the same classes and onClick handler (toggleClientSecret) and add accessibility
attributes such as aria-label (e.g., "Toggle client secret visibility") and
aria-pressed={state.showClientSecret}; ensure the inner <i> still uses the
cx(...) logic for icon classes and the element remains focusable and styled like
the original control.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 27152bc and 9f47e37.

📒 Files selected for processing (2)
  • front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx
  • server/services/tuya/lib/tuya.disconnect.js

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (1)
front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx (1)

488-488: ⚠️ Potential issue | 🟡 Minor

Use autoComplete prop casing in JSX.

autocomplete at Line 488 should be autoComplete to match JSX prop conventions.

#!/bin/bash
# Verify JSX prop casing for autocomplete usage in this component.
rg -nP '\bautocomplete\s*=' front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx` at line 488,
In the SetupTab.jsx component change the JSX prop name from lowercase
"autocomplete" to the correct React/JSX casing "autoComplete" on the
input/element where it's currently set (search for the attribute in SetupTab JSX
markup); update any other occurrences found by the provided ripgrep command to
ensure consistent camelCase prop usage and run the component build/test to
verify no props are missed.
🧹 Nitpick comments (4)
server/services/tuya/lib/tuya.loadDeviceDetails.js (1)

34-50: Consider extracting repeated error-logging logic.

The reason extraction pattern is duplicated four times. A small helper would reduce repetition.

♻️ Optional refactor
+const logIfRejected = (result, label) => {
+  if (result.status === 'rejected') {
+    const reason = result.reason?.message ?? result.reason;
+    logger.warn(`[Tuya] Failed to load ${label} for ${deviceId}: ${reason}`);
+  }
+};
+
+logIfRejected(specResult, 'specifications');
+logIfRejected(detailsResult, 'details');
+logIfRejected(propsResult, 'properties');
+logIfRejected(modelResult, 'thing model');
-  if (specResult.status === 'rejected') {
-    const reason = specResult.reason && specResult.reason.message ? specResult.reason.message : specResult.reason;
-    logger.warn(`[Tuya] Failed to load specifications for ${deviceId}: ${reason}`);
-  }
-  if (detailsResult.status === 'rejected') {
-    const reason =
-      detailsResult.reason && detailsResult.reason.message ? detailsResult.reason.message : detailsResult.reason;
-    logger.warn(`[Tuya] Failed to load details for ${deviceId}: ${reason}`);
-  }
-  if (propsResult.status === 'rejected') {
-    const reason = propsResult.reason && propsResult.reason.message ? propsResult.reason.message : propsResult.reason;
-    logger.warn(`[Tuya] Failed to load properties for ${deviceId}: ${reason}`);
-  }
-  if (modelResult.status === 'rejected') {
-    const reason = modelResult.reason && modelResult.reason.message ? modelResult.reason.message : modelResult.reason;
-    logger.warn(`[Tuya] Failed to load thing model for ${deviceId}: ${reason}`);
-  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/services/tuya/lib/tuya.loadDeviceDetails.js` around lines 34 - 50,
Extract the repeated reason-extraction and logging into a small helper function
(e.g., getRejectionReason(result) and logRejection(result, label, deviceId)) and
replace the four duplicate blocks that use specResult, detailsResult,
propsResult, and modelResult with calls to that helper; getRejectionReason
should return result.reason.message if present or result.reason otherwise, and
logRejection should call logger.warn(`[Tuya] Failed to load ${label} for
${deviceId}: ${reason}`) only when result.status === 'rejected', keeping the
original labels ("specifications", "details", "properties", "thing model").
server/test/services/tuya/lib/utils/tuya.deviceParams.test.js (1)

30-35: Test descriptions are swapped relative to their content.

The test at line 30 claims "using device param constants" but actually uses plain string literals ('A', 'B'). Conversely, the test at line 51 has a generic description but actually uses DEVICE_PARAM_NAME constants. Consider swapping the descriptions to match the actual test content.

🔧 Proposed fix
-  it('should get param value using device param constants', () => {
+  it('should get param value', () => {
     const value = getParamValue([{ name: 'A', value: 42 }], 'A');
     expect(value).to.equal(42);
     expect(getParamValue([{ name: 'A', value: 42 }], 'B')).to.equal(undefined);
     expect(getParamValue(null, 'A')).to.equal(undefined);
   });
-  it('should get param value', () => {
+  it('should get param value using device param constants', () => {
     const params = [
       { name: DEVICE_PARAM_NAME.IP_ADDRESS, value: '1.1.1.1' },
       { name: DEVICE_PARAM_NAME.PROTOCOL_VERSION, value: '3.3' },
     ];

Also applies to: 51-58

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

In `@server/test/services/tuya/lib/utils/tuya.deviceParams.test.js` around lines
30 - 35, The two unit tests around getParamValue have their descriptions
reversed: the test that uses plain string keys ('A', 'B') is labeled to use
device param constants and the test that actually uses DEVICE_PARAM_NAME
constants has a generic description; update the test descriptions so the one
invoking getParamValue([{ name: 'A', value: 42 }], 'A') reads something like
"should get param value using plain param names" and the one that passes
DEVICE_PARAM_NAME constants reads "should get param value using device param
constants" to match the actual test code (refer to the getParamValue calls and
DEVICE_PARAM_NAME usage to locate the tests).
server/services/tuya/lib/mappings/index.js (1)

137-141: Align external_id code parsing with the polling path.

This parser uses parts[2], while server/services/tuya/lib/tuya.poll.js (getFeatureCode) uses the last segment. Using a fixed index here is more brittle and can drift from polling behavior.

♻️ Proposed fix
-    const parts = String(feature.external_id).split(':');
-    if (parts.length >= 3) {
-      const code = parts[2];
+    const parts = String(feature.external_id).split(':');
+    if (parts.length >= 2) {
+      const code = parts[parts.length - 1];
       if (code) {
         codes.add(String(code).toLowerCase());
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/services/tuya/lib/mappings/index.js` around lines 137 - 141, The code
in mappings/index.js extracts the feature code using parts[2], which can diverge
from the polling implementation getFeatureCode in
server/services/tuya/lib/tuya.poll.js that uses the last segment; modify the
parser in mappings/index.js (the block that splits feature.external_id and
currently reads parts[2]) to instead take the last segment (e.g.,
parts[parts.length - 1]) and normalize it the same way getFeatureCode does
(string coercion and toLowerCase) so both parsers remain consistent.
server/test/services/tuya/lib/tuya.localPoll.test.js (1)

16-146: Consider extracting a shared rejection assertion helper.

The repeated try/catch + throw new Error('Expected error') pattern appears across multiple tests. A small helper would reduce duplication and make future error-path additions less noisy.

Also applies to: 189-223

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

In `@server/test/services/tuya/lib/tuya.localPoll.test.js` around lines 16 - 146,
Tests repeat the same try/catch + throw pattern to assert errors; create a small
helper (e.g., assertRejectsWith) that takes a function returning a promise, an
expected error class (BadParameters) and expected message or substring, and use
it to replace the repeated try/catch blocks in the tests that call localPoll
(the tests that currently define TuyAPIStub/TuyAPINewGenStub and then await
localPoll with invalid responses). Update each failing-path test (the ones
asserting instanceOf BadParameters and message checks) to call
assertRejectsWith(() => localPoll({...}), BadParameters, '...') instead of the
try/catch + throw to remove duplication while preserving the same assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx`:
- Around line 56-63: getVariable is swallowing all errors and returning fallback
for backend/network failures; change it to only return fallback for a
404/NotFound (or explicit "variable missing") response and for other errors
either log the error via this.props.logger/processLogger and rethrow or
propagate the exception so the caller can handle it. Locate getVariable and the
similar fetch helpers around the SetupTab component (references: getVariable,
this.props.httpClient.get) and update their catch blocks to inspect the
error/response status: if status indicates missing variable return fallback,
otherwise log the full error and rethrow (or return a rejected Promise). Apply
the same change to the other fetch routines in lines 65–93 so real
network/backend failures are not silently ignored.

In `@server/services/tuya/lib/mappings/index.js`:
- Around line 155-158: The current logic sets codes =
extractCodesFromSpecifications(specifications) and only falls back to
extractCodesFromFeatures(device.features) if that set is empty, which drops
feature-derived codes when specs are partial; change this to merge both sources
instead: call extractCodesFromSpecifications(specifications) and
extractCodesFromFeatures(device.features) and take the union into codes so both
spec and feature codes are considered when selecting mappings (refer to
extractCodesFromSpecifications, extractCodesFromFeatures, the codes variable,
and device.features to locate where to combine the sets).

In `@server/services/tuya/lib/tuya.poll.js`:
- Around line 223-224: When localPoll succeeds but localResult.dps is
missing/invalid, set an explicit fallback reason (e.g., fallback =
'invalid-local-dps') before falling back to cloud so logs/tracing show the root
cause; update the branch around the localResult/dps check in tuya.poll.js (the
block referencing localResult, dps and the conditional if (dps && typeof dps ===
'object')) and apply the same change to the analogous block around lines 286-289
so the fallback variable is set to a descriptive value whenever DPS is absent or
not an object.
- Around line 123-125: The loop that assigns values[feature.code] from result
(in the result.forEach callback) can throw when items are null or not objects;
update the result.forEach((feature) => { ... }) callback to validate each item
before using feature.code/value — e.g., skip if feature is falsy or typeof
feature !== 'object' or feature.code is undefined — and only perform
values[feature.code] = feature.value when those checks pass (optionally log or
count skipped items for visibility).

In `@server/test/services/tuya/lib/tuya.localPoll.test.js`:
- Around line 225-258: Rename or update the two failing tests so their
descriptions match what they assert: either change the it(...) titles to
describe that they expect the connect error to be thrown (e.g., for the first
test that currently reads "should log last socket error when different from
thrown error" change to something like "should throw connect error when connect
rejects") or add explicit assertions that the socket error was logged/handled
(mock and assert processLogger.error or the instance.on('error') side effect)
and for the second test (the one around "stop cleanup when already resolved")
similarly align the title with the asserted thrown cleanup error or add
assertions that verify cleanup stopping behavior; update the tests that call
localPoll (the localPoll invocation and TuyAPIStub/setup) and their expect(...)
calls accordingly so test names and asserted behavior match.
- Around line 4-8: Replace the current top-level proxyquire setup and direct
import of updateDiscoveredDeviceAfterLocalPoll by using
proxyquire().noPreserveCache() and removing the direct require of
updateDiscoveredDeviceAfterLocalPoll; instead, defer loading the function via
proxyquire when each test needs it so the module is reloaded with stubs applied.
Specifically, change the proxyquire initialization to call noPreserveCache() on
the proxyquire require and stop importing updateDiscoveredDeviceAfterLocalPoll
at module scope—load it through proxyquire inside the test that needs
updateDiscoveredDeviceAfterLocalPoll so stubs take effect.

---

Duplicate comments:
In `@front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx`:
- Line 488: In the SetupTab.jsx component change the JSX prop name from
lowercase "autocomplete" to the correct React/JSX casing "autoComplete" on the
input/element where it's currently set (search for the attribute in SetupTab JSX
markup); update any other occurrences found by the provided ripgrep command to
ensure consistent camelCase prop usage and run the component build/test to
verify no props are missed.

---

Nitpick comments:
In `@server/services/tuya/lib/mappings/index.js`:
- Around line 137-141: The code in mappings/index.js extracts the feature code
using parts[2], which can diverge from the polling implementation getFeatureCode
in server/services/tuya/lib/tuya.poll.js that uses the last segment; modify the
parser in mappings/index.js (the block that splits feature.external_id and
currently reads parts[2]) to instead take the last segment (e.g.,
parts[parts.length - 1]) and normalize it the same way getFeatureCode does
(string coercion and toLowerCase) so both parsers remain consistent.

In `@server/services/tuya/lib/tuya.loadDeviceDetails.js`:
- Around line 34-50: Extract the repeated reason-extraction and logging into a
small helper function (e.g., getRejectionReason(result) and logRejection(result,
label, deviceId)) and replace the four duplicate blocks that use specResult,
detailsResult, propsResult, and modelResult with calls to that helper;
getRejectionReason should return result.reason.message if present or
result.reason otherwise, and logRejection should call logger.warn(`[Tuya] Failed
to load ${label} for ${deviceId}: ${reason}`) only when result.status ===
'rejected', keeping the original labels ("specifications", "details",
"properties", "thing model").

In `@server/test/services/tuya/lib/tuya.localPoll.test.js`:
- Around line 16-146: Tests repeat the same try/catch + throw pattern to assert
errors; create a small helper (e.g., assertRejectsWith) that takes a function
returning a promise, an expected error class (BadParameters) and expected
message or substring, and use it to replace the repeated try/catch blocks in the
tests that call localPoll (the tests that currently define
TuyAPIStub/TuyAPINewGenStub and then await localPoll with invalid responses).
Update each failing-path test (the ones asserting instanceOf BadParameters and
message checks) to call assertRejectsWith(() => localPoll({...}), BadParameters,
'...') instead of the try/catch + throw to remove duplication while preserving
the same assertions.

In `@server/test/services/tuya/lib/utils/tuya.deviceParams.test.js`:
- Around line 30-35: The two unit tests around getParamValue have their
descriptions reversed: the test that uses plain string keys ('A', 'B') is
labeled to use device param constants and the test that actually uses
DEVICE_PARAM_NAME constants has a generic description; update the test
descriptions so the one invoking getParamValue([{ name: 'A', value: 42 }], 'A')
reads something like "should get param value using plain param names" and the
one that passes DEVICE_PARAM_NAME constants reads "should get param value using
device param constants" to match the actual test code (refer to the
getParamValue calls and DEVICE_PARAM_NAME usage to locate the tests).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f47e37 and 63cebed.

📒 Files selected for processing (7)
  • front/src/routes/integration/all/tuya/setup-page/SetupTab.jsx
  • server/services/tuya/lib/mappings/index.js
  • server/services/tuya/lib/tuya.init.js
  • server/services/tuya/lib/tuya.loadDeviceDetails.js
  • server/services/tuya/lib/tuya.poll.js
  • server/test/services/tuya/lib/tuya.localPoll.test.js
  • server/test/services/tuya/lib/utils/tuya.deviceParams.test.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant