Skip to content

refactor(inovelli): extract shared logic to src/lib/inovelli.ts#11761

Open
rohankapoorcom wants to merge 2 commits intoKoenkk:masterfrom
rohankapoorcom:inovelli-lib-cleanup
Open

refactor(inovelli): extract shared logic to src/lib/inovelli.ts#11761
rohankapoorcom wants to merge 2 commits intoKoenkk:masterfrom
rohankapoorcom:inovelli-lib-cleanup

Conversation

@rohankapoorcom
Copy link
Contributor

@rohankapoorcom rohankapoorcom commented Mar 20, 2026

This is the second piece in the next phase of my refactor for the Inovelli converters. Cleaning this up moves all of Inovelli's functionality over to a new file in /src/lib/ and leaves behind the device specific configuration in the converter file itself.

This refactor was planned and executed with assistance from Cursor.

Execution plan (click to expand)

Inovelli lib cleanup plan

Current state

  • src/devices/inovelli.ts is ~3026 lines and contains:
  • Zcl cluster interfaces (Inovelli, InovelliMmWave)
  • Lookup tables and constants (click/button/led/mmWave, cluster names)
  • Custom cluster definitions and inovelliExtend (device/addCluster/light/fan/MMWave/energyReset)
  • Attribute interfaces and large attribute maps (COMMON_ATTRIBUTES, COMMON_DIMMER_*, VZM30_ATTRIBUTESVZM36_ATTRIBUTES)
  • Helper functions (attributesToExposeList, chunkedRead, intToFanMode, speedToInt, createMmWaveCompositeAreaConverter)
  • All fromZigbee (fzLocal) and toZigbee (tzLocal) converters
  • Expose builders (exposeLedEffects, exposeBreezeMode, exposeMMWave*, etc.)
  • A single definitions export (5 devices: VZM30-SN, VZM31-SN, VZM32-SN, VZM35-SN, VZM36)

Target pattern (from existing libs)

  • philips: src/lib/philips.ts holds cluster defs, converters, and modern extend; exports m, tz, fz, and helpers. src/devices/philips.ts imports * as philips from "../lib/philips" and only defines devices (plus device-only tzLocal where needed).
  • ledvance: src/lib/ledvance.ts exports ledvanceFz, ledvanceTz, and extend helpers; device file imports and composes.
  • ikea: src/lib/ikea.ts exports ikeaLight(), ikeaBattery(), etc.; device file imports and uses them.

Approach

Create src/lib/inovelli.ts and move all shared logic into it; keep src/devices/inovelli.ts as a thin file that imports from the lib and only declares the five device definitions.


1. Add src/lib/inovelli.ts

Move the following from the device file into the new lib (preserving structure and types):

  • Imports: Zcl from zigbee-herdsman, Parameter type, fz/tz from converters, exposes, m (modernExtend), reporting, types from ../lib/types, utils, and for chunkedRead / cluster reads: Zh (endpoint type).
  • Interfaces: Inovelli, InovelliMmWave, Attribute, BreezeModeValues.
  • Constants and lookups: clickLookup, buttonLookup, ledEffects, individualLedEffects, mmWaveControlCommands, INOVELLI_CLUSTER_NAME, INOVELLI_MMWAVE_CLUSTER_NAME, INOVELLI (0x122f), FAN_MODES, BREEZE_MODES, LED_NOTIFICATION_TYPES, BUTTON_TAP_SEQUENCES.
  • Attribute maps: Keep the existing composition hierarchy in the lib (no structural change). COMMON_ATTRIBUTES is the base; COMMON_DIMMER_ATTRIBUTES, COMMON_DIMMABLE_LIGHT_ATTRIBUTES, and COMMON_DIMMER_ON_OFF_ATTRIBUTES extend it; each VZM*_ATTRIBUTES composes from these with device-specific overrides (e.g. VZM31 temporarily excludes fanTimerMode). Add a short comment block in the lib documenting this layering. Export only the device-facing attribute sets: VZM30_ATTRIBUTES, VZM31_ATTRIBUTES, VZM32_ATTRIBUTES, VZM32_MMWAVE_ATTRIBUTES, VZM35_ATTRIBUTES, VZM36_ATTRIBUTES. Keep COMMON_* internal (not exported) so the device file only uses inovelli.VZM30_ATTRIBUTES, etc.; composition stays an implementation detail and the API stays small.
  • Helpers: intToFanMode, speedToInt, attributesToExposeList, chunkedRead, createMmWaveCompositeAreaConverter.
  • Extend object: The whole inovelliExtend object (addCustomClusterInovelli, addCustomMMWaveClusterInovelli, inovelliDevice, inovelliLight, inovelliFan, inovelliMMWave, inovelliEnergyReset).
  • Converters: All of tzLocal and fzLocal (move as-is; they reference the constants and types above).
  • Expose helpers: exposeLedEffects, exposeIndividualLedEffects, exposeBreezeMode, exposeMMWaveControl, exposeLedEffectComplete, exposeEnergyReset, exposeMMWaveAreas, createAreaComposite, exposeDetectionAreas, exposeInterferenceAreas, exposeStayAreas.

Exports from the lib (follow philips/ledvance style):

  • Cluster names (no double "Inovelli"): Export as CLUSTER_NAME and MMWAVE_CLUSTER_NAME so the device file uses inovelli.CLUSTER_NAME and inovelli.MMWAVE_CLUSTER_NAME. Internally the lib can keep INOVELLI_CLUSTER_NAME / INOVELLI_MMWAVE_CLUSTER_NAME and re-export: export const CLUSTER_NAME = INOVELLI_CLUSTER_NAME and export const MMWAVE_CLUSTER_NAME = INOVELLI_MMWAVE_CLUSTER_NAME.
  • export { inovelliExtend as m } (device file uses only inovelli.m.*).
  • tz/fz: Keep internal to the lib (do not export). No device references individual converters at the definition level; all five devices (including VZM36) use only extend. See "tz/fz export" below.
  • Export only the device-facing attribute maps: VZM30_ATTRIBUTES, VZM31_ATTRIBUTES, VZM32_ATTRIBUTES, VZM32_MMWAVE_ATTRIBUTES, VZM35_ATTRIBUTES, VZM36_ATTRIBUTES (device file uses inovelli.VZM30_ATTRIBUTES, etc.). Do not export COMMON_ATTRIBUTES or COMMON_DIMMER_* / COMMON_DIMMABLE_LIGHT_* / COMMON_DIMMER_ON_OFF_*; they remain internal building blocks.
  • Export any type needed by the device file only if it becomes part of the public API (e.g. if a type is re-exported for device-level typing); otherwise keep types internal to the lib.

Use a single const e = exposes.presets and const ea = exposes.access (and exposes where needed) inside the lib file. The lib will depend on ../converters/fromZigbee, ../converters/toZigbee, ../lib/exposes, ../lib/modernExtend, ../lib/reporting, ../lib/types, ../lib/utils; path adjustments: from src/lib/inovelli.ts use ../converters/... and ./... for other libs.


2. Shrink src/devices/inovelli.ts

  • Imports:
  • import * as inovelli from "../lib/inovelli";
  • import * as m from "../lib/modernExtend";
  • Only any other symbols still needed for the definitions (e.g. DefinitionWithExtend if not re-exported from lib).
  • Body: Only the definitions array. Each definition should:
  • Use inovelli.m.* for extend (e.g. inovelli.m.addCustomCluster(), inovelli.m.device(...), inovelli.m.light(), etc. — see naming convention below).
  • Use inovelli.CLUSTER_NAME and inovelli.MMWAVE_CLUSTER_NAME where cluster names are required (no double "Inovelli").
  • Use inovelli.VZM30_ATTRIBUTES, inovelli.VZM31_ATTRIBUTES, etc. where attribute sets are required.
  • Use inovelli.fz.* and inovelli.tz.* for any converters referenced in definitions (e.g. VZM36's fromZigbee/toZigbee that reference fzLocal.brightness, fzLocal.vzm36_fan_light_state, tzLocal.vzm36_fan_on_off).
  • Remove from the device file: all interfaces, constants, attribute maps, helpers, inovelliExtend, tzLocal, fzLocal, and all expose helpers. No duplicate logic should remain.

Result: the device file is on the order of ~130 lines (imports + five definition objects).


3. Naming and export convention

  • Cluster names: Export as CLUSTER_NAME and MMWAVE_CLUSTER_NAME so usage is inovelli.CLUSTER_NAME / inovelli.MMWAVE_CLUSTER_NAME (Inovelli appears only once, in the namespace).
  • Extend methods: Rename so the device file does not repeat "inovelli" in the chain. Export the extend object with shorter method names, e.g.:
  • addCustomClusterInovelliaddCustomCluster
  • addCustomMMWaveClusterInovelliaddCustomMMWaveCluster
  • inovelliDevicedevice
  • inovelliLightlight
  • inovelliFanfan
  • inovelliMMWavemmWave
  • inovelliEnergyResetenergyReset
    So usage is inovelli.m.device(...), inovelli.m.light(), inovelli.m.addCustomCluster(), etc.
  • Converters: Keep tz/fz internal (not exported); see "tz/fz export" below.
  • Do not change behavior of converters or extend logic; this is a pure move, rename, and wiring of imports/exports.

4. tz/fz export: do we need to export individual converters?

No. Keep tz/fz internal to the lib.

All five devices (VZM30-SN through VZM36) now use only extend at the definition level. VZM36 has been refactored to use the same pattern as the others: extend: [inovelli.m.light({ splitValuesByEndpoint: true }), inovelli.m.fan({ endpointId: 2, splitValuesByEndpoint: true }), inovelli.m.device(...), inovelli.m.addCustomCluster(), m.identify()] with fromZigbee: [] and toZigbee: []. The extend methods push the right converters internally (e.g. on_off_for_endpoint, brightness, parameterized fan_state(endpointId)). The device file never references individual converters; the lib's extend (m) is the only public API, and tz/fz stay internal (not exported).


5. Attribute objects (handling the big composed objects)

  • Keep the hierarchy as-is in the lib: COMMON_ATTRIBUTESCOMMON_DIMMER_ATTRIBUTES, COMMON_DIMMABLE_LIGHT_ATTRIBUTES, COMMON_DIMMER_ON_OFF_ATTRIBUTES → device-specific VZM*_ATTRIBUTES (with spreads and overrides). No refactor of the composition logic.
  • Export only VZM30_ATTRIBUTES, VZM31_ATTRIBUTES, VZM32_ATTRIBUTES, VZM32_MMWAVE_ATTRIBUTES, VZM35_ATTRIBUTES, VZM36_ATTRIBUTES. The device file never references COMMON_*; those stay internal.
  • Document in the lib: Add a brief comment above the attribute section, e.g. "Attribute composition: COMMON_ATTRIBUTES is the base; COMMON_DIMMER_, COMMON_DIMMABLE_LIGHT_, COMMON_DIMMER_ON_OFF_ extend it; VZM_ATTRIBUTES are device-specific compositions. Only VZM* are exported."

6. Cleanup inside converters (optional, within scope of move)

  • Extract VZM36 endpoint/key resolution: The pattern "split key on _, if two parts use endpoint N and base key" is duplicated in inovelli_parameters (convertSet + convertGet) and inovelli_parameters_readOnly (convertGet). Add a small helper in the lib, e.g. resolveEndpointAndKey(entity, key, meta): { entityToUse, keyToUse }, and use it in those three places so the logic lives in one place and is easier to maintain.
  • Typing comments: Leave existing // @ts-expect-error ignore and // XXX: far too dynamic to properly type as-is during the move (no behavior or type changes). These are known limits; improving them can be a follow-up if desired.
  • No other converter refactors in this change: keep the move focused so behavior and tests stay unchanged.

7. Staged rollout (multiple PRs for easier review)

Break the work into four stages. Each stage is a separate, mergeable change that leaves the codebase buildable and tests passing. Later stages depend on earlier ones.


Stage 1: Create lib with types, constants, attributes, and pure helpers

  • Add src/lib/inovelli.ts containing only:
  • Interfaces: Inovelli, InovelliMmWave, Attribute, BreezeModeValues.
  • Constants and lookups: cluster names (internal INOVELLI_CLUSTER_NAME / INOVELLI_MMWAVE_CLUSTER_NAME), INOVELLI, clickLookup, buttonLookup, ledEffects, individualLedEffects, mmWaveControlCommands, FAN_MODES, BREEZE_MODES, LED_NOTIFICATION_TYPES, BUTTON_TAP_SEQUENCES.
  • Attribute maps: full hierarchy (COMMON_* internal, VZM*_ATTRIBUTES composed as today) plus a short comment documenting the composition. Export only VZM30_ATTRIBUTESVZM36_ATTRIBUTES.
  • Pure helpers: intToFanMode, speedToInt, attributesToExposeList, chunkedRead, createMmWaveCompositeAreaConverter (and export attributesToExposeList, chunkedRead if the device file still needs them in this stage).
  • Exports: CLUSTER_NAME, MMWAVE_CLUSTER_NAME, INOVELLI, lookups (or only what the device file needs), VZM30_ATTRIBUTESVZM36_ATTRIBUTES, and types Attribute, Inovelli, InovelliMmWave, BreezeModeValues so the device file can type annotations. Export pure helpers that the device file's extend will call.
  • Device file: Add import * as inovelli from "../lib/inovelli". Replace every use of the moved constants, attribute maps, types, and pure helpers with inovelli.*. Delete the corresponding definitions from the device file (interfaces, constants, attribute maps, pure helpers). Leave inovelliExtend, tzLocal, fzLocal, and expose helpers in the device file; they now reference inovelli.CLUSTER_NAME, inovelli.ledEffects, etc.
  • Review focus: No behavior change. Lib is "data + pure helpers" only; device file is slimmer and wires to the lib.

Stage 2: Move extend object and expose helpers to lib

  • In lib: Add the full extend object (custom clusters + device, light, fan, mmWave, energyReset with the planned method renames) and all expose helpers (including exposeMMWaveTargets). Export the extend as m.
  • Device file: Replace every inovelliExtend.* call with inovelli.m.* (using the new method names). VZM36 already uses only extend (same pattern as others: light({ splitValuesByEndpoint: true }), fan({ endpointId: 2, splitValuesByEndpoint: true }), device(...), addCustomCluster(), m.identify()) with empty fromZigbee/toZigbee. Remove the local inovelliExtend and all expose helpers.
  • Review focus: Extend and expose logic now live in the lib; device file only calls inovelli.m.*. No behavior change.

Stage 3: Move converters to lib (and optional DRY helper)

  • In lib: Add all of tzLocal and fzLocal (including on_off_for_endpoint, fan_state(endpointId), report_target_info, etc.); they are used only internally by the extend. Do not export tz/fz. Optionally add resolveEndpointAndKey(entity, key, meta) and use it in the three places that duplicate the endpoint/key logic for split endpoints.
  • Device file: Remove the local tzLocal and fzLocal (no definitions reference them; all devices use only extend).
  • Review focus: All Inovelli converters live in the lib and are internal; device file is now just definitions + imports. No behavior change (or only the small DRY cleanup if done).

Stage 4: Final polish

  • Add the attribute-composition comment in the lib if not already present. Ensure all renames (cluster names, extend method names) are applied and consistent. Run pnpm run build, pnpm run check, pnpm test. Optionally run a quick smoke check that a device (e.g. VZM31-SN) still resolves and exposes as before.
  • Review focus: Documentation and consistency; no new logic.

Summary of stages

  • Stage 1 — Lib gains: types, constants, attributes, pure helpers. Device file: imports inovelli; uses inovelli.* for all of the above; removes duplicated definitions. Mergeable: Yes.
  • Stage 2 — Lib gains: extend object + expose helpers (exported as m). Device file: uses inovelli.m.*; removes local extend + expose helpers. Mergeable: Yes.
  • Stage 3 — Lib gains: converters moved to lib (internal only; not exported); optional resolveEndpointAndKey. Device file: removes local converters (no device references them at definition level). Mergeable: Yes.
  • Stage 4 — Lib gains: comment + consistency pass. Device file: none (or trivial). Mergeable: Yes.

After Stage 3, the device file is minimal (~130 lines). Stage 4 is optional polish and can be folded into Stage 3 if you prefer fewer PRs.


8. Verification (per stage and final)

  • After each stage: run pnpm run build, pnpm run check, and pnpm test; fix any path/type or test failures before merging.
  • Optionally after Stage 3: run a quick smoke test that a device (e.g. VZM31-SN) still resolves and exposes the same (e.g. via existing tests or manual check).

9. Summary

  • src/lib/inovelli.ts (new): All shared Inovelli logic (types, constants, attribute maps, helpers, custom clusters, extend object, fromZigbee/toZigbee converters internal, expose builders including exposeMMWaveTargets). Exports: CLUSTER_NAME, MMWAVE_CLUSTER_NAME, attribute maps, m (extend with methods device, light, fan, addCustomCluster, addCustomMMWaveCluster, mmWave, energyReset). tz/fz stay internal; no device references them.
  • src/devices/inovelli.ts: Imports from ../lib/inovelli and ../lib/modernExtend; only the five definitions entries. All devices (including VZM36) use only inovelli.m.* (light, fan, device, addCustomCluster, mmWave, energyReset with appropriate options), inovelli.CLUSTER_NAME, inovelli.MMWAVE_CLUSTER_NAME, and inovelli.VZM*_ATTRIBUTES. No references to inovelli.tz or inovelli.fz.

This matches the existing "vendor lib + thin device file" pattern used by philips, ikea, and ledvance and keeps device definitions in one place while making Inovelli logic reusable and testable from src/lib/inovelli.ts.

rohankapoorcom and others added 2 commits March 19, 2026 17:31
Move types, constants, attribute maps, helpers, extend, and converters
into a new lib; export cluster names, VZM*_ATTRIBUTES, and m (extend).
Device file is now ~130 lines with five definitions only. Add docs for
attribute composition and resolveEndpointAndKey; drop unused
@ts-expect-error in led_effect converter.

Co-authored-by: Cursor <cursoragent@cursor.com>
@rohankapoorcom
Copy link
Contributor Author

@InovelliUSA This probably needs fairly extensive testing from your side. AFAIK, there should be no code functionality changes, just a whole bunch of refactoring in place. Of course we have no unit tests to validate functionality... so 😀

@InovelliUSA
Copy link
Contributor

@rohankapoorcom I will run this on my network, but let me know if you make any modifications to it so I can update it in my environment.

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.

2 participants