Skip to content

Unite attributes values & enable strict mode#54

Merged
heavyrubberslave merged 20 commits into
mainfrom
feat/unite-attributes-values
Dec 29, 2025
Merged

Unite attributes values & enable strict mode#54
heavyrubberslave merged 20 commits into
mainfrom
feat/unite-attributes-values

Conversation

@heavyrubberslave

@heavyrubberslave heavyrubberslave commented Dec 23, 2025

Copy link
Copy Markdown
Member

Fixes #53

Summary by CodeRabbit

  • Refactor

    • Unified attribute model: devices and virtual devices now use keyed, typed attributes for consistent access.
    • Many updater/handler flows converted to async/await for predictable asynchronous behavior.
  • Bug Fixes / Reliability

    • Hardened startup, CORS and discovery flows; improved guards and connection/error handling.
    • More robust transport and device update paths to reduce intermittent failures.
  • New Features

    • Added typed attribute kinds, branded numeric helpers, and richer attribute validation.
    • Enhanced virtual device behaviors (TTS, display, random generator) with typed attributes.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 23, 2025

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Replaces legacy GenericDeviceAttribute arrays with a typed DeviceAttribute map model, adds branded numeric types and utilities, converts device updater APIs and callers to async/Promise, and updates protocol implementations, virtual devices, factories, serializers, tests, and tooling to the new attribute-first design.

Changes

Cohort / File(s) Summary
Core attribute model
src/device/attribute/deviceAttribute.ts, src/device/attribute/numberDeviceAttribute.ts, src/device/attribute/strDeviceAttribute.ts, src/device/attribute/boolDeviceAttribute.ts, src/device/attribute/intDeviceAttribute.ts, src/device/attribute/floatDeviceAttribute.ts, src/device/attribute/intRangeDeviceAttribute.ts, src/device/attribute/listDeviceAttribute.ts
Introduced DeviceAttribute base and typed subclasses (str/number/int/float/range/list/bool) with factory helpers, parsing/validation, modifiers, serialization exposures; removed legacy *GenericDeviceAttribute files.
Device core & helpers
src/device/device.ts, src/util/numbers.ts, src/util/objects.ts
Made Device generic over keyed DeviceAttributes, switched storage to attribute map, added branded Int/Float helpers and getTypedKeys utility.
Updater API & callers (async)
src/device/updater/*, src/device/genericDeviceUpdater.ts, src/socket/deviceUpdateHandler.ts, src/controller/patchDeviceController.ts, src/index.ts
Converted DeviceUpdaterInterface and implementations to return Promise<void>; callers (controllers, socket handlers, buffered updater) now await update calls; PATCH route wrapped with asyncHandler.
Virtual devices & factories
src/device/protocol/virtual/*
VirtualDevice/VirtualDeviceLogic made generic; factories now return VirtualDevice; virtual logics return keyed attribute objects and use typed accessors and setAttribute generics.
Protocol implementations & factories
src/device/protocol/buttplugIo/*, src/device/protocol/slvCtrlPlus/*, src/device/protocol/zc95/*
Reworked protocol device classes, factories and parsers to use keyed DeviceAttribute maps, typed setAttribute, branded numeric usage, defensive nullability and updated transport/message interactions.
Repositories / Providers / Services
src/repository/*, src/device/provider/*, src/serviceProvider/deviceServiceProvider.ts, src/device/provider/deviceProviderLoader.ts
Removed DeviceData generics from public repository/provider signatures (use plain Device), consolidated provider factory lookup, adjusted device removal and settings access patterns.
Serialization, settings & domain objects
src/serialization/*, src/settings/*, src/settings/knownDevice.ts, src/settings/deviceSource.ts
Updated discriminators to new attribute classes; KnownDevice and DeviceSource now use private fields + getters and constructors; settings storage optional/constructed; serializers adjusted.
Tests
tests/unit/**
Updated tests to use attribute maps and new DeviceAttribute factories; getAttribute assertions use .value; constructor args adapted and new negative test added.
Removed modules
src/device/updater/delegateDeviceUpdater.ts, legacy *GenericDeviceAttribute.ts files
Deleted delegate updater and legacy GenericDeviceAttribute files replaced by the new DeviceAttribute system.
Misc & tooling
.eslintrc.cjs, tsconfig.json, package.json, src/logging/*, src/serial/*, src/automation/*, src/version.ts
Enabled TS strict, stricter ESLint rules, typed logger child API, defensive nullability updates, dependency edits, and minor control-flow refinements.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant HTTP as "HTTP Server"
  participant Controller as "PatchDeviceController"
  participant Updater as "GenericDeviceUpdater"
  participant Device
  participant Transport as "DeviceTransport"

  Client->>HTTP: PATCH /device/:id (payload)
  HTTP->>Controller: invoke execute(req,res)
  activate Controller
  Controller->>Updater: await update(device, rawData)
  activate Updater
  loop per attribute
    Updater->>Device: await getAttribute(key)
    Device-->>Updater: attribute or undefined
    alt attribute exists & writable
      Updater->>Device: await setAttribute(key, parsedValue)
      Device->>Transport: send protocol command (if applicable)
      Transport-->>Device: ack/response
      Device-->>Updater: updated attribute.value
    else
      Updater-->>Updater: log skip or error
    end
  end
  Updater-->>Controller: resolve
  deactivate Updater
  Controller-->>HTTP: send 200 OK
  deactivate Controller
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Zc95 support #50 — High overlap: broad refactor of device attribute model and updater async APIs that likely intersects with ZC95 and protocol updates in this PR.
  • Fix cors origin handling #47 — Related: changes to CORS / ALLOWED_ORIGINS handling in src/index.ts that touch the same file and origin logic.

Poem

"I hopped through keys and typed each trait,
Maps over lists — I cleaned the gate.
Awaited updates, no more race,
Branded numbers snug in place.
A rabbit claps — code feels great! 🐇"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main refactoring work: consolidating device attribute structures and enabling TypeScript strict mode.
Linked Issues check ✅ Passed The PR successfully unites device attributes and their data structures across all device types by replacing separate attribute arrays with typed attribute maps and removing generic attribute classes.
Out of Scope Changes check ✅ Passed All changes relate to the core objective: refactoring attribute handling (new DeviceAttribute hierarchy, device-specific attribute types, async updates), enabling strict mode, and supporting infrastructure (logger injection, type utilities). No unrelated modifications detected.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69e86fe and 81e857e.

📒 Files selected for processing (4)
  • src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts
  • src/device/protocol/virtual/genericVirtualDeviceFactory.ts
  • src/device/protocol/virtual/virtualDeviceLogic.ts
  • src/serviceProvider/deviceServiceProvider.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/serviceProvider/deviceServiceProvider.ts
🧰 Additional context used
🧬 Code graph analysis (3)
src/device/protocol/virtual/virtualDeviceLogic.ts (2)
src/device/protocol/virtual/virtualDevice.ts (2)
  • VirtualDevice (7-58)
  • refreshData (39-41)
src/device/protocol/virtual/display/displayVirtualDeviceLogic.ts (2)
  • DisplayVirtualDeviceLogic (5-23)
  • configureAttributes (16-22)
src/device/protocol/virtual/genericVirtualDeviceFactory.ts (4)
src/logging/Logger.ts (1)
  • Logger (9-19)
src/device/protocol/virtual/virtualDeviceLogic.ts (1)
  • VirtualDeviceLogic (4-11)
src/device/protocol/virtual/virtualDeviceFactory.ts (1)
  • VirtualDeviceFactory (4-9)
src/factory/dateFactory.ts (1)
  • DateFactory (1-7)
src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts (7)
src/device/attribute/strDeviceAttribute.ts (1)
  • StrDeviceAttribute (6-36)
src/device/attribute/boolDeviceAttribute.ts (1)
  • BoolDeviceAttribute (6-36)
src/device/attribute/intDeviceAttribute.ts (1)
  • IntDeviceAttribute (8-42)
src/device/protocol/virtual/virtualDeviceLogic.ts (1)
  • VirtualDeviceLogic (4-11)
src/logging/Logger.ts (1)
  • Logger (9-19)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/device/protocol/virtual/virtualDevice.ts (1)
  • VirtualDevice (7-58)
🔇 Additional comments (6)
src/device/protocol/virtual/virtualDeviceLogic.ts (1)

1-11: LGTM! Well-designed generic interface.

The generic type parameter with a sensible default (DeviceAttributes) makes this interface type-safe while maintaining backward compatibility. The method signatures correctly reference the type parameter.

src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts (4)

12-17: Well-structured attribute type definition.

The TtsVirtualDeviceAttributes type clearly maps each attribute to its concrete type, providing type safety for attribute access throughout the implementation.


28-35: Logger dependency properly integrated.

The constructor accepts the logger injected by GenericVirtualDeviceFactory and creates a child logger for structured logging. This aligns with the factory changes in this PR.


37-80: Previous review feedback successfully addressed.

The refreshData method now:

  • Uses optional chaining for attribute access (lines 38-40) ✓
  • Sets text to undefined instead of null (line 48) ✓
  • Properly handles errors in the speech callback with .catch() (lines 76-77) ✓

The defensive check at line 66 for undefined textToSpeak appears redundant given the empty-array guard at line 51, but it provides safety against edge cases and is acceptable for a virtual device.


86-119: Attribute configuration correctly migrated to new system.

The method properly uses factory methods (create and createInitialized) to construct typed attributes with appropriate modifiers and initial values. The returned object matches the TtsVirtualDeviceAttributes type definition.

src/device/protocol/virtual/genericVirtualDeviceFactory.ts (1)

6-7: Logger dependency properly threaded through factory.

The logger is correctly injected into device logic instances via the constructor. The return type specialization from Promise<Device> to Promise<VirtualDevice> is safe since VirtualDevice extends Device. All instantiations in deviceServiceProvider.ts (RandomGenerator, Display, and Tts factories) have been updated to pass the logger parameter.


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

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

@heavyrubberslave

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Dec 23, 2025

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 7

Caution

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

⚠️ Outside diff range comments (1)
src/device/protocol/buttplugIo/buttplugIoDevice.ts (1)

46-56: sendValue is undefined for FloatGenericDeviceAttribute.

The ButtplugIoDeviceAttributes type includes FloatGenericDeviceAttribute, but the sendValue calculation only handles RangeGenericDeviceAttribute and BoolGenericDeviceAttribute. For Float attributes, sendValue will be undefined, causing incorrect behavior when calling this.send().

         if (attrDef instanceof RangeGenericDeviceAttribute) {
             sendValue = Number(value)/attrDef.max;
         } else if (attrDef instanceof BoolGenericDeviceAttribute) {
             sendValue = value ? 1 : 0;
+        } else if (attrDef instanceof FloatGenericDeviceAttribute) {
+            sendValue = Number(value);
         }
🧹 Nitpick comments (15)
src/device/genericDeviceUpdater.ts (1)

27-29: Consider using try/catch for consistency with async/await.

Mixing await with .then()/.catch() works but is less idiomatic. For consistency with the async function style, consider using try/catch instead.

🔎 Alternative implementation using try/catch
-            await device.setAttribute(attrKey, attrStr)
-                .then(() => this.logger.info(`${deviceLogMsg} -> done`))
-                .catch((e: Error) => this.logger.error(`${deviceLogMsg} -> failed: ${e.message}`, e))
+            try {
+                await device.setAttribute(attrKey, attrStr);
+                this.logger.info(`${deviceLogMsg} -> done`);
+            } catch (e) {
+                this.logger.error(`${deviceLogMsg} -> failed: ${e.message}`, e);
+            }
src/device/attribute/intGenericDeviceAttribute.ts (1)

9-11: Consider validating the numeric conversion.

The Number(value) conversion can return NaN for invalid inputs, which might propagate through the system unexpectedly. Consider adding validation to ensure valid integer values.

🔎 Proposed validation
 public fromString(value: string): number {
-    return Number(value);
+    const num = Number(value);
+    if (isNaN(num)) {
+        throw new Error(`Invalid integer value: ${value}`);
+    }
+    return num;
 }
src/device/attribute/floatGenericDeviceAttribute.ts (1)

5-7: Consider validating the numeric conversion.

Similar to IntGenericDeviceAttribute, the Number(value) conversion can return NaN for invalid inputs. Consider adding validation to ensure valid float values.

🔎 Proposed validation
 public fromString(value: string): number {
-    return Number(value);
+    const num = Number(value);
+    if (isNaN(num)) {
+        throw new Error(`Invalid float value: ${value}`);
+    }
+    return num;
 }
src/device/protocol/virtual/delegatedVirtualDeviceFactory.ts (1)

9-21: Remove unnecessary Promise wrapper.

The factory.create(knownDevice, provider) already returns a Promise<VirtualDevice>, so wrapping it in another Promise constructor is redundant. Simply return the promise directly.

🔎 Simplified implementation
 public async create(knownDevice: KnownDevice, provider: string): Promise<VirtualDevice> {
-    return new Promise<VirtualDevice>((resolve) => {
-        const factoryName = `${DelegatedVirtualDeviceFactory.capitalizeFirstLetter(knownDevice.type)}VirtualDeviceLogic`;
-        if (!this.deviceFactories.has(factoryName)) {
-            throw new Error(`No factory defined for virtual device '${knownDevice.type}'`);
-        }
-
-        const factory = this.deviceFactories.get(factoryName);
-        const device = factory.create(knownDevice, provider);
-
-        resolve(device);
-    })
+    const factoryName = `${DelegatedVirtualDeviceFactory.capitalizeFirstLetter(knownDevice.type)}VirtualDeviceLogic`;
+    if (!this.deviceFactories.has(factoryName)) {
+        throw new Error(`No factory defined for virtual device '${knownDevice.type}'`);
+    }
+
+    const factory = this.deviceFactories.get(factoryName);
+    return factory.create(knownDevice, provider);
 }
src/device/protocol/virtual/genericVirtualDeviceFactory.ts (1)

23-41: Simplify synchronous Promise construction.

The entire operation is synchronous, so wrapping it in a Promise constructor is unnecessary. Consider using Promise.resolve() or returning the device directly if the interface allows.

🔎 Simplified implementation
 public create(knownDevice: KnownDevice, provider: string): Promise<VirtualDevice>
 {
-    return new Promise<VirtualDevice>((resolve) => {
-        const deviceLogic = new this.ctor(knownDevice.config);
-
-        const device = new VirtualDevice(
-            "1.0.0",
-            knownDevice.id,
-            knownDevice.name,
-            knownDevice.type,
-            provider,
-            this.dateFactory.now(),
-            knownDevice.config,
-            deviceLogic
-        );
-
-        resolve(device);
-    });
+    const deviceLogic = new this.ctor(knownDevice.config);
+
+    const device = new VirtualDevice(
+        "1.0.0",
+        knownDevice.id,
+        knownDevice.name,
+        knownDevice.type,
+        provider,
+        this.dateFactory.now(),
+        knownDevice.config,
+        deviceLogic
+    );
+
+    return Promise.resolve(device);
 }
src/device/protocol/virtual/virtualDeviceProvider.ts (1)

70-82: Consider keeping type safety and removing redundant rethrow.

Two observations:

  1. Line 70: Removing the explicit as VirtualDevice cast could cause type mismatches since connectedDevices is declared as Map<string, VirtualDevice> (line 15). If the factory returns a Device but not specifically a VirtualDevice, TypeScript should flag this.

  2. Lines 81-82: Rethrowing the error after logging is redundant in an async function. The error is already logged and will naturally propagate as a rejected promise without the explicit throw.

🔎 Proposed refinement
-            const device = await this.deviceFactory.create(knowDevice, VirtualDeviceProvider.name);
+            const device = await this.deviceFactory.create(knowDevice, VirtualDeviceProvider.name) as VirtualDevice;
             const deviceStatusUpdaterInterval = this.initDeviceStatusUpdater(device);
 
             this.connectedDevices.set(knowDevice.id, device);
@@ -78,8 +78,6 @@
             this.logger.info('Connected virtual devices: ' + this.connectedDevices.size.toString());
         } catch (e: unknown) {
             this.logger.error(`Could not initiate virtual device '${knowDevice.id}': ${(e as Error).message}`, e);
-
-            throw e;
         }
     }
src/device/attribute/listGenericDeviceAttribute.ts (1)

8-11: Consider documenting type assertion assumptions.

The fromString method parses integers when possible and casts the result to K. While this works when K includes both string and number (union type), the type assertion as K assumes the caller has validated that the parsed type matches K.

This is acceptable given the constraint K extends string|number, but consider adding a comment explaining this assumption or validating against the actual K type if possible.

💡 Optional: Add clarifying comment
 public fromString(value: string): K {
+    // Parse as int if possible; K is constrained to string|number so cast is safe
     const parsedInt = parseInt(value, 10);
     return (isNaN(parsedInt) ? value : parsedInt) as K;
 }
src/device/protocol/virtual/randomGenerator/randomGeneratorVirtualDeviceLogic.ts (1)

36-44: Consider initializing the attribute value.

The valueAttr is created without an initial value. Since refreshData sets it immediately on the first refresh cycle, this is likely acceptable, but for consistency and to avoid potential undefined reads before the first refresh, consider initializing it:

         const valueAttr = new IntGenericDeviceAttribute();
         valueAttr.name = 'value';
         valueAttr.modifier = GenericDeviceAttributeModifier.readOnly;
+        valueAttr.value = 0;
src/device/protocol/virtual/virtualDevice.ts (1)

47-57: Consider simplifying with async/await instead of Promise constructor.

Wrapping synchronous operations in a new Promise() constructor is an anti-pattern. Since the operations are synchronous, you can simplify:

-    public setAttribute<K extends keyof T>(attributeName: K, value: T[K]['value']): Promise<T[K]['value']> {
-        return new Promise<T[K]['value']>((resolve) => {
-            this.state = DeviceState.busy;
-
-            this.attributes[attributeName].value = value;
-
-            this.state = DeviceState.ready;
-
-            resolve(value);
-        });
+    public async setAttribute<K extends keyof T>(attributeName: K, value: T[K]['value']): Promise<T[K]['value']> {
+        this.state = DeviceState.busy;
+        this.attributes[attributeName].value = value;
+        this.state = DeviceState.ready;
+        return value;
     }
src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts (1)

74-76: Reassigning value parameter creates type confusion.

Reassigning the value parameter from boolean to number (0 or 1) can cause type confusion since the original type is DeviceAttributes[K]['value']. Consider using a separate variable:

+            let sendValue: string | number | boolean = value;
             if (value === true || value === false) {
-                value = value ? 1 : 0;
+                sendValue = value ? 1 : 0;
             }

-            const result = await this.send(`set-${attributeName.toString()} ${value}`);
+            const result = await this.send(`set-${attributeName.toString()} ${sendValue}`);
src/device/protocol/buttplugIo/buttplugIoDevice.ts (1)

58-58: Unnecessary template literal.

The template literal wrapping attributeName is redundant since it's already a string key:

-        this.attributes[`${attributeName}`].value = value;
+        this.attributes[attributeName].value = value;
src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts (2)

32-34: Consider using static attribute name constants consistently.

The class defines static readonly constants for attribute names (lines 18-21), but getAttribute calls use string literals. For consistency and refactoring safety, use the constants:

-        const text = (await device.getAttribute('text')).value;
-        const queuing = (await device.getAttribute('queuing')).value;
-        const speaking = (await device.getAttribute('speaking')).value;
+        const text = (await device.getAttribute(TtsVirtualDeviceLogic.textAttrName)).value;
+        const queuing = (await device.getAttribute(TtsVirtualDeviceLogic.queuingAttrName)).value;
+        const speaking = (await device.getAttribute(TtsVirtualDeviceLogic.speakingAttrName)).value;

73-96: Attributes created without initial values.

The attributes are instantiated without initial value properties. Consider initializing them to avoid potential undefined reads before the first refresh:

         const textAttr = new StrGenericDeviceAttribute();
         textAttr.name = TtsVirtualDeviceLogic.textAttrName;
         textAttr.modifier = GenericDeviceAttributeModifier.writeOnly;
+        textAttr.value = null; // or empty string if null not allowed

         const speakingAttr = new BoolGenericDeviceAttribute();
         speakingAttr.name = TtsVirtualDeviceLogic.speakingAttrName;
         speakingAttr.modifier = GenericDeviceAttributeModifier.readOnly;
+        speakingAttr.value = false;

         const queuingAttr = new BoolGenericDeviceAttribute();
         queuingAttr.name = TtsVirtualDeviceLogic.queuingAttrName;
         queuingAttr.modifier = GenericDeviceAttributeModifier.readWrite;
+        queuingAttr.value = false;

         const queueLengthAttr = new IntGenericDeviceAttribute();
         queueLengthAttr.name = TtsVirtualDeviceLogic.queueLengthAttrName;
         queueLengthAttr.modifier = GenericDeviceAttributeModifier.readOnly;
+        queueLengthAttr.value = 0;
src/device/protocol/zc95/zc95Device.ts (2)

197-228: Async conversion looks correct.

The Promise wrapper and loop logic safely process queued messages. The attribute access has been properly updated to use this.attributes.

Optional: Consider simplifying the Promise wrapper

Since there are no asynchronous operations within the loop, the method could be simplified:

-private processQueuedMessages(): Promise<void> {
-    return new Promise((resolve) => {
-        let queueMessagesProcessed = 0;
-
-        while (this.receiveQueue.length > 0 && queueMessagesProcessed <= 10) {
+private async processQueuedMessages(): Promise<void> {
+    let queueMessagesProcessed = 0;
+    
+    while (this.receiveQueue.length > 0 && queueMessagesProcessed <= 10) {
+        const msg = this.receiveQueue.shift();
+        ++queueMessagesProcessed;
         // ... rest of logic ...
-        }
-
-        resolve();
-    });
+    }
 }

However, the current implementation is perfectly valid.


230-262: Consider reusing the attrName variable.

The method correctly builds pattern attributes, but Line 258 reconstructs the attribute key instead of reusing the attrName variable defined on Line 236.

Suggested refactor to reduce redundancy
 for (const menuItem of menuItems) {
     const attrName = `${Zc95Device.patternAttributePrefix}${menuItem.Id}`;
     
     if (this.isMinMaxMenuItem(menuItem)) {
         const rangeAttr = new RangeGenericDeviceAttribute();
         rangeAttr.name = attrName;
         // ... rest of range setup ...
         patternAttributes[attrName as keyof Zc95DevicePatternAttributes] = rangeAttr;
     } else if (this.isMultiChoiceMenuItem(menuItem)) {
         const listAttr = new ListGenericDeviceAttribute<number, string>();
         listAttr.name = attrName;
         // ... rest of list setup ...
         patternAttributes[attrName as keyof Zc95DevicePatternAttributes] = listAttr;
     }
     
-    patternAttributes[`${Zc95Device.patternAttributePrefix}${menuItem.Id}`].value = menuItem.Default;
+    patternAttributes[attrName as keyof Zc95DevicePatternAttributes].value = menuItem.Default;
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c81cf04 and 5787c61.

📒 Files selected for processing (43)
  • src/controller/patchDeviceController.ts
  • src/device/attribute/boolGenericDeviceAttribute.ts
  • src/device/attribute/floatGenericDeviceAttribute.ts
  • src/device/attribute/genericDeviceAttribute.ts
  • src/device/attribute/intGenericDeviceAttribute.ts
  • src/device/attribute/listGenericDeviceAttribute.ts
  • src/device/attribute/rangeGenericDeviceAttribute.ts
  • src/device/attribute/strGenericDeviceAttribute.ts
  • src/device/device.ts
  • src/device/genericDeviceUpdater.ts
  • src/device/protocol/buttplugIo/buttplugIoDevice.ts
  • src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts
  • src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts
  • src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts
  • src/device/protocol/slvCtrlPlus/slvCtrlPlusMessageParser.ts
  • src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts
  • src/device/protocol/virtual/delegatedVirtualDeviceFactory.ts
  • src/device/protocol/virtual/display/displayVirtualDeviceLogic.ts
  • src/device/protocol/virtual/genericVirtualDeviceFactory.ts
  • src/device/protocol/virtual/randomGenerator/randomGeneratorVirtualDeviceLogic.ts
  • src/device/protocol/virtual/virtualDevice.ts
  • src/device/protocol/virtual/virtualDeviceFactory.ts
  • src/device/protocol/virtual/virtualDeviceLogic.ts
  • src/device/protocol/virtual/virtualDeviceProvider.ts
  • src/device/protocol/zc95/zc95Device.ts
  • src/device/protocol/zc95/zc95DeviceFactory.ts
  • src/device/provider/deviceProvider.ts
  • src/device/updater/abstractDeviceUpdater.ts
  • src/device/updater/bufferedDeviceUpdater.ts
  • src/device/updater/delegateDeviceUpdater.ts
  • src/device/updater/deviceUpdaterInterface.ts
  • src/index.ts
  • src/repository/connectedDeviceRepository.ts
  • src/repository/deviceRepositoryInterface.ts
  • src/serialization/classToPlainSerializer.ts
  • src/serviceProvider/deviceServiceProvider.ts
  • src/socket/deviceUpdateHandler.ts
  • tests/unit/controller/getDevicesController.spec.ts
  • tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts
  • tests/unit/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.spec.ts
  • tests/unit/device/protocol/slvCtrlPlus/slvCtrlPlusMessageParser.spec.ts
  • tests/unit/device/protocol/virtual/display/displayVirtualDevice.spec.ts
  • tests/unit/device/testDevice.ts
💤 Files with no reviewable changes (1)
  • src/device/updater/delegateDeviceUpdater.ts
🧰 Additional context used
🧬 Code graph analysis (26)
src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts (1)
src/device/device.ts (1)
  • DeviceAttributes (5-5)
src/device/protocol/virtual/delegatedVirtualDeviceFactory.ts (1)
src/device/protocol/virtual/virtualDeviceFactory.ts (1)
  • VirtualDeviceFactory (4-9)
src/device/updater/abstractDeviceUpdater.ts (1)
src/device/device.ts (1)
  • DeviceData (7-9)
src/device/updater/deviceUpdaterInterface.ts (1)
src/device/device.ts (1)
  • DeviceData (7-9)
src/socket/deviceUpdateHandler.ts (1)
src/socket/types.ts (1)
  • DeviceUpdateData (3-3)
src/controller/patchDeviceController.ts (2)
src/middleware/contentTypeMiddleware.ts (1)
  • req (3-18)
src/device/device.ts (1)
  • DeviceData (7-9)
src/device/genericDeviceUpdater.ts (1)
src/device/device.ts (1)
  • DeviceData (7-9)
src/device/attribute/genericDeviceAttribute.ts (2)
src/device/protocol/buttplugIo/buttplugIoDevice.ts (1)
  • Exclude (10-78)
src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts (1)
  • Exclude (8-100)
tests/unit/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.spec.ts (2)
src/device/device.ts (1)
  • DeviceAttributes (5-5)
src/device/transport/deviceTransport.ts (1)
  • DeviceTransport (1-26)
src/device/protocol/virtual/randomGenerator/randomGeneratorVirtualDeviceLogic.ts (2)
src/device/attribute/intGenericDeviceAttribute.ts (1)
  • IntGenericDeviceAttribute (4-12)
src/device/protocol/virtual/virtualDeviceLogic.ts (1)
  • VirtualDeviceLogic (4-11)
tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts (1)
src/device/protocol/buttplugIo/buttplugIoDevice.ts (1)
  • ButtplugIoDeviceAttributes (8-8)
src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts (4)
src/device/attribute/strGenericDeviceAttribute.ts (1)
  • StrGenericDeviceAttribute (3-7)
src/device/attribute/boolGenericDeviceAttribute.ts (1)
  • BoolGenericDeviceAttribute (3-8)
src/device/attribute/intGenericDeviceAttribute.ts (1)
  • IntGenericDeviceAttribute (4-12)
src/device/protocol/virtual/virtualDeviceLogic.ts (1)
  • VirtualDeviceLogic (4-11)
src/device/protocol/virtual/virtualDevice.ts (2)
src/device/device.ts (1)
  • DeviceAttributes (5-5)
src/device/protocol/virtual/virtualDeviceLogic.ts (1)
  • VirtualDeviceLogic (4-11)
src/device/provider/deviceProvider.ts (1)
src/device/device.ts (1)
  • DeviceAttributes (5-5)
src/device/protocol/buttplugIo/buttplugIoDevice.ts (3)
src/device/attribute/rangeGenericDeviceAttribute.ts (1)
  • RangeGenericDeviceAttribute (4-20)
src/device/attribute/boolGenericDeviceAttribute.ts (1)
  • BoolGenericDeviceAttribute (3-8)
src/device/attribute/floatGenericDeviceAttribute.ts (1)
  • FloatGenericDeviceAttribute (3-8)
src/device/protocol/slvCtrlPlus/slvCtrlPlusMessageParser.ts (1)
src/device/device.ts (1)
  • DeviceAttributes (5-5)
src/device/protocol/zc95/zc95DeviceFactory.ts (2)
src/device/protocol/zc95/zc95Device.ts (1)
  • Zc95DeviceAttributes (32-33)
src/device/attribute/listGenericDeviceAttribute.ts (1)
  • ListGenericDeviceAttribute (4-12)
src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts (1)
src/device/device.ts (1)
  • DeviceAttributes (5-5)
src/device/protocol/zc95/zc95Device.ts (4)
src/device/attribute/listGenericDeviceAttribute.ts (1)
  • ListGenericDeviceAttribute (4-12)
src/device/attribute/boolGenericDeviceAttribute.ts (1)
  • BoolGenericDeviceAttribute (3-8)
src/device/attribute/rangeGenericDeviceAttribute.ts (1)
  • RangeGenericDeviceAttribute (4-20)
src/device/protocol/zc95/Zc95Messages.ts (2)
  • MinMaxMenuItem (46-51)
  • MultiChoiceMenuItem (53-55)
src/device/protocol/virtual/virtualDeviceLogic.ts (1)
src/device/device.ts (1)
  • DeviceAttributes (5-5)
src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts (2)
src/device/protocol/buttplugIo/buttplugIoDevice.ts (1)
  • ButtplugIoDeviceAttributes (8-8)
src/device/attribute/floatGenericDeviceAttribute.ts (1)
  • FloatGenericDeviceAttribute (3-8)
src/serviceProvider/deviceServiceProvider.ts (1)
src/device/deviceManager.ts (1)
  • DeviceManager (7-64)
src/device/updater/bufferedDeviceUpdater.ts (2)
src/device/device.ts (1)
  • DeviceData (7-9)
src/device/updater/deviceUpdaterInterface.ts (1)
  • DeviceUpdaterInterface (3-6)
src/device/protocol/virtual/display/displayVirtualDeviceLogic.ts (2)
src/device/attribute/strGenericDeviceAttribute.ts (1)
  • StrGenericDeviceAttribute (3-7)
src/device/protocol/virtual/virtualDeviceLogic.ts (1)
  • VirtualDeviceLogic (4-11)
tests/unit/device/protocol/slvCtrlPlus/slvCtrlPlusMessageParser.spec.ts (6)
src/device/attribute/boolGenericDeviceAttribute.ts (1)
  • BoolGenericDeviceAttribute (3-8)
src/device/attribute/rangeGenericDeviceAttribute.ts (1)
  • RangeGenericDeviceAttribute (4-20)
src/device/attribute/intGenericDeviceAttribute.ts (1)
  • IntGenericDeviceAttribute (4-12)
src/device/attribute/listGenericDeviceAttribute.ts (1)
  • ListGenericDeviceAttribute (4-12)
src/device/attribute/strGenericDeviceAttribute.ts (1)
  • StrGenericDeviceAttribute (3-7)
src/device/attribute/floatGenericDeviceAttribute.ts (1)
  • FloatGenericDeviceAttribute (3-8)
src/index.ts (1)
src/middleware/contentTypeMiddleware.ts (1)
  • req (3-18)
🔇 Additional comments (57)
src/serviceProvider/deviceServiceProvider.ts (2)

11-11: LGTM! Import style updated consistently.

The change from a named import to a default import for Device is consistent with how other modules like DeviceManager are imported in this file.


61-61: LGTM! Type parameter correctly aligned with DeviceManager signature.

The removal of the nested <DeviceData> generic parameter correctly aligns the instantiation with DeviceManager's constructor signature, which expects Map<string, Device> without the nested generic. This change is part of the broader refactor to simplify the device attribute type system.

src/device/updater/deviceUpdaterInterface.ts (1)

5-5: LGTM! Appropriate async refactor.

The change from synchronous to asynchronous (Promise) is appropriate for device update operations that likely involve I/O. The AI summary confirms this aligns with async implementations across the codebase.

src/device/provider/deviceProvider.ts (3)

3-3: LGTM! Consistent with attribute refactor.

The import change from DeviceData to DeviceAttributes aligns with the broader refactoring of the device attribute system.


21-26: LGTM! Method chaining enabled.

Changing the return type from DeviceProvider to this enables fluent method chaining, which is a good design practice for builder-style APIs.


28-28: LGTM! Generic constraint updated.

The constraint change from T extends DeviceData to T extends DeviceAttributes is consistent with the attribute refactoring across the codebase.

src/device/protocol/zc95/zc95DeviceFactory.ts (1)

65-81: LGTM! Consistent migration to typed attribute objects.

The refactor from array-based attributes to a strongly-typed object structure (Zc95DeviceAttributes) with named properties is well-executed. The use of ListGenericDeviceAttribute<number, string> with proper type parameters ensures type safety for the active pattern attribute.

tests/unit/device/protocol/virtual/display/displayVirtualDevice.spec.ts (1)

32-32: LGTM! Test updated for new attribute structure.

The assertion now correctly accesses the .value property of the attribute object returned by getAttribute(), which aligns with the refactored attribute model where attributes are objects containing a value field.

tests/unit/controller/getDevicesController.spec.ts (1)

21-21: LGTM! Test updated for object-based attributes.

The constructor argument change from [] to {} correctly reflects the migration from array-based to object-based device attributes.

tests/unit/device/testDevice.ts (1)

11-11: LGTM! Test helper updated for new attribute model.

The constructor call correctly changes the attributes parameter from an empty array [] to an empty object {}, consistent with the migration to object-based device attributes.

src/device/protocol/virtual/display/displayVirtualDeviceLogic.ts (3)

5-7: LGTM! Well-defined attribute type.

The DisplayVirtualDeviceAttributes type provides clear, strongly-typed structure for the display device's attributes.


9-9: LGTM! Generic type parameter added.

Implementing VirtualDeviceLogic<DisplayVirtualDeviceAttributes> ensures type safety across the virtual device logic interface.


20-28: LGTM! Consistent migration to typed attribute objects.

The method now returns a strongly-typed object structure instead of an array, with the return value matching the DisplayVirtualDeviceAttributes type. This provides better type safety and clearer attribute access patterns.

src/index.ts (1)

104-107: LGTM! Proper async error handling.

Wrapping the PATCH route with asyncHandler is the correct approach now that the controller's execute method is asynchronous. This ensures any rejected promises are properly caught and passed to Express's error handling middleware.

tests/unit/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.spec.ts (1)

1-37: LGTM! Well-structured test for attribute validation.

The test correctly verifies that attempting to set a non-existent attribute results in a rejected promise with the appropriate error message. The test follows AAA pattern and properly uses mocking for the transport layer.

src/device/updater/abstractDeviceUpdater.ts (1)

14-14: LGTM! Async update contract established.

Changing the abstract update method to return Promise<void> properly establishes an asynchronous contract for all device updater implementations. This aligns with the broader refactor making device operations asynchronous.

src/device/protocol/virtual/virtualDeviceFactory.ts (1)

2-6: LGTM! Improved type specificity.

Refining the return type from Promise<Device> to Promise<VirtualDevice> provides better type safety and more accurate interface documentation. This aligns with the generic attribute refactor across virtual device implementations.

src/device/attribute/boolGenericDeviceAttribute.ts (1)

3-7: LGTM! Type-safe boolean attribute specialization.

Extending GenericDeviceAttribute<boolean> and narrowing the fromString return type to boolean provides better type safety and aligns with the generic attribute model introduced across the codebase.

src/controller/patchDeviceController.ts (2)

19-19: LGTM! Controller properly made async.

The method signature correctly updated to async and returns Promise<void>, aligning with the asynchronous device updater contract.


30-30: LGTM! Update call properly awaited.

The deviceUpdater.update call is now correctly awaited, ensuring the asynchronous update operation completes before proceeding. This aligns with the new Promise<void> contract of the updater interface.

src/device/attribute/strGenericDeviceAttribute.ts (1)

3-7: LGTM! Type-safe string attribute specialization.

Extending GenericDeviceAttribute<string> and implementing fromString as an identity function provides proper type safety while maintaining consistency with other attribute type specializations in the codebase.

src/socket/deviceUpdateHandler.ts (2)

24-24: LGTM! Handler properly made async.

The method signature correctly updated to async and returns Promise<void>, enabling proper awaiting of the asynchronous device update operation.


33-33: LGTM! Update operation properly awaited.

The deviceUpdater.update call is now correctly awaited, ensuring the asynchronous update completes before the handler returns. Error handling is preserved and will properly catch any rejected promises.

src/repository/deviceRepositoryInterface.ts (1)

1-8: LGTM!

The type simplification from Device<DeviceData> to Device aligns well with the broader refactor to a generic attributes-based model. The changes are consistent across both methods.

src/repository/connectedDeviceRepository.ts (1)

1-21: LGTM!

The type updates correctly implement the interface changes from DeviceRepositoryInterface and maintain consistency with the attribute model refactor.

src/device/protocol/virtual/virtualDeviceLogic.ts (1)

4-8: All VirtualDeviceLogic implementations have been updated correctly.

Verified that the three implementations (RandomGeneratorVirtualDeviceLogic, TtsVirtualDeviceLogic, DisplayVirtualDeviceLogic) all return their respective typed attributes matching the new interface signature that returns T instead of GenericDeviceAttribute[].

src/device/genericDeviceUpdater.ts (1)

19-19: The condition is correct and does not have a falsy value handling issue.

The getAttribute method returns Promise<T[K] | undefined> where T[K] is an attribute object (not a primitive value). When an attribute exists, it returns the attribute object which is always truthy; when it doesn't exist, it returns undefined which is falsy. Therefore, !await device.getAttribute(attrKey) correctly checks attribute existence, not the truthiness of the attribute's value. The code properly skips only attributes that don't exist, regardless of their .value property.

Likely an incorrect or invalid review comment.

src/device/protocol/virtual/virtualDeviceProvider.ts (2)

9-9: LGTM: Import addition aligns with type refactoring.

The Device import supports the type generalization in the removeDevice method.


86-86: LGTM: Type generalization improves flexibility.

Changing the parameter type from VirtualDevice to Device makes the method more flexible while maintaining type safety.

src/device/attribute/rangeGenericDeviceAttribute.ts (2)

4-4: LGTM: Generic type parameter aligns with attribute refactor.

Extending GenericDeviceAttribute<number> is consistent with other numeric attribute types (IntGenericDeviceAttribute, FloatGenericDeviceAttribute) and properly constrains the value type.


17-19: LGTM: fromString implementation is appropriate.

The method correctly converts string input to number and aligns with similar implementations in other numeric attribute classes.

tests/unit/device/protocol/slvCtrlPlus/slvCtrlPlusMessageParser.spec.ts (3)

21-54: LGTM: Test updates correctly reflect object-based attribute access.

The test assertions have been systematically updated to:

  • Use Object.keys(result).length for counting attributes
  • Access attributes by name (e.g., result.connected) instead of array indices
  • Cast specific attribute types where needed

These changes correctly validate the new DeviceAttributes object-based structure.


77-90: LGTM: Edge case tests properly updated.

The empty and malformed attribute tests correctly use the object-based API and verify expected behavior.


101-106: LGTM: Malformed attribute test correctly validates object access.

The test properly verifies that valid attributes are accessible by name (result.bar) while malformed ones are ignored.

src/device/updater/bufferedDeviceUpdater.ts (1)

16-27: LGTM: Async refactor properly implemented.

The update flow has been correctly converted to async/await:

  • Public update method now properly awaits the queue operation
  • Private handleUpdate awaits the nested updater call
  • Aligns with the DeviceUpdaterInterface signature expecting Promise<void>

This ensures proper completion tracking for buffered updates.

src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts (3)

2-2: LGTM: Import supports generic attribute typing.

The DeviceAttributes import enables the generic constraint on the class.


6-6: LGTM: Generic parameter properly constrained with sensible default.

The class declaration:

  • Introduces generic parameter A extends DeviceAttributes
  • Provides default type DeviceAttributes for backward compatibility
  • Extends Device<A> to maintain type flow

This allows concrete device classes to specify their exact attribute structure while maintaining type safety.


17-17: LGTM: Constructor signature updated consistently.

The attributes parameter type correctly uses the generic A, ensuring type consistency throughout the inheritance chain.

src/device/protocol/slvCtrlPlus/slvCtrlPlusMessageParser.ts (3)

8-8: LGTM: Import enables object-based attribute return type.

The DeviceAttributes type import supports the refactored return type.


20-22: LGTM: Method signature and initialization updated for object-based attributes.

Changes properly reflect the shift from array to object:

  • Return type now DeviceAttributes (Record<string, GenericDeviceAttribute>)
  • Initialized as empty object {} as DeviceAttributes

36-46: LGTM: Parsing logic correctly builds attribute object.

The implementation properly:

  • Renames loop variable to attrDef for clarity (definition string)
  • Creates attribute via createAttributeFromValue
  • Assigns to object using attribute name as key: attributeList[attr.name] = attr

This correctly builds the keyed attribute map.

tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts (5)

4-6: LGTM: Import includes strongly-typed attribute container.

The ButtplugIoDeviceAttributes type import enables type-safe test construction.


13-24: LGTM: Test helper updated to use object-based attributes.

The createDevice function signature correctly uses ButtplugIoDeviceAttributes parameter type, aligning with the new attribute model.


30-30: LGTM: Empty attributes object for error case.

Passing an empty object {} correctly tests the non-existing attribute error path.


49-62: LGTM: Boolean attribute test updated correctly.

The test properly:

  • Creates attribute object with keyed structure: {'bool-1': boolAttr}
  • Accesses result value via (await device.getAttribute(boolAttr.name)).value

74-93: LGTM: Range attribute test updated correctly.

The test uses the same object-based pattern and properly validates the scaled scalar calculation.

src/device/attribute/listGenericDeviceAttribute.ts (1)

4-6: LGTM: Generic type parameters properly introduced.

The class now uses:

  • Generic parameters <K extends string|number, V extends string|number>
  • Extends GenericDeviceAttribute<K> for type safety
  • Properly typed values: Map<K, V>

This enables strongly-typed list attributes with specific key/value types.

src/device/attribute/genericDeviceAttribute.ts (2)

3-4: Well-designed generic abstraction for typed attributes.

The introduction of AttributeValue as a union type and making GenericDeviceAttribute generic with a constrained type parameter is a clean approach. Making fromString abstract ensures each concrete attribute type handles its own parsing logic.

Also applies to: 12-12, 28-28


25-26: The value property initialization follows an intentional pattern for this DTO class. While technically uninitialized, it's always populated before use through plainToInstance() deserialization or direct assignment. The current design is appropriate for the class-transformer serialization pattern used throughout the codebase.

Likely an incorrect or invalid review comment.

src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts (1)

55-86: Clean refactor from array to map-based attributes.

The transition to returning ButtplugIoDeviceAttributes as a keyed object is well-implemented. The explicit initialization of attr.value = 0 for each attribute addresses potential undefined value issues.

src/device/device.ts (3)

93-102: Good defensive typing for getAttribute.

The documentation explaining that undefined can be returned when dealing with Partial types is helpful. The implementation correctly accesses the attributes map.


5-6: Well-structured generic refactor of the Device base class.

The transition to a generic DeviceAttributes type parameter with corresponding changes to the constructor and abstract setAttribute signature provides strong typing throughout the device hierarchy.

Also applies to: 12-12, 39-39, 47-47, 104-104


7-9: No changes needed. The DeviceData type is actively used throughout the codebase as an exported type, imported and utilized in socket types, controllers, and device updater classes. This is the intended pattern for this type definition.

src/device/protocol/zc95/zc95Device.ts (4)

13-34: Strong type system improvements!

The new attribute type structure is well-designed:

  • Clear separation between required and optional attributes
  • Good use of template literal types for dynamic pattern attributes
  • Type safety improvements over the previous array-based approach

69-166: Well-structured refactoring of setAttribute logic.

The generic constraint K extends keyof Zc95DeviceAttributes provides excellent type safety, and the refactored setter methods correctly use the new attribute-based model. The recursive call at Line 142 is safe due to the guard condition at Line 149.


168-184: Helper methods properly refactored.

All helper methods have been correctly updated:

  • getChannelPowerAttributes returns a properly typed object structure
  • removePatternAttributesAndData uses the type-safe getTypedKeys helper
  • getTypedKeys is a useful utility for maintaining type safety during iteration
  • Type guard isPowerChannelAttribute signature correctly updated

Also applies to: 187-195, 264-268, 282-284


36-36: Breaking changes verified and properly implemented throughout the codebase.

The class signature, constructor, and setAttribute method changes are all correctly implemented:

  • The Device base class supports the generic constraint with Device<T extends DeviceAttributes>
  • Zc95Device properly extends Device<Zc95DeviceAttributes> with the correct generic parameter
  • The constructor accepts attributes: Zc95DeviceAttributes and passes it to the parent class
  • setAttribute uses proper generic constraints: setAttribute<K extends keyof Zc95DeviceAttributes>(attributeName: K, value: Zc95DeviceAttributes[K]['value'])
  • Zc95DeviceFactory.create() correctly instantiates Zc95Device with all parameters in the proper order, including the new Zc95DeviceAttributes type

All breaking changes have been consistently applied across the codebase.

Comment thread src/device/protocol/buttplugIo/buttplugIoDevice.ts Outdated
Comment thread src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts Outdated
Comment thread src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts Outdated
Comment thread src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts Outdated
Comment thread src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts Outdated
Comment thread src/device/protocol/virtual/virtualDevice.ts Outdated
Comment thread src/serialization/classToPlainSerializer.ts Outdated
@heavyrubberslave

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Dec 26, 2025

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 7

Caution

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

⚠️ Outside diff range comments (3)
src/serial/SynchronousSerialPort.ts (1)

28-38: Fix race condition: promise resolves before write completes.

The promise resolves immediately on line 36 without waiting for the write operation to complete. This creates a race where resolve() is called before the write callback can potentially call reject(err), meaning the method may return successfully even if the write fails.

🔎 Proposed fix
 public async write(data: string): Promise<void> {
     return new Promise<void>((resolve, reject) => {
         this.writer.write(data, (err: Error | null | undefined) => {
             if (err) {
                 reject(err);
+            } else {
+                resolve();
             }
         });
-
-        resolve();
     });
 }
src/automation/scriptRuntime.ts (2)

80-92: Inconsistent cleanup in stop() method.

The stop() method sets vm, sandbox, and runningSince to null but leaves scriptCode untouched and doesn't null logWriter after closing it. This creates ambiguous state after stopping.

For consistent cleanup semantics, all nullable runtime resources should be handled uniformly. After stop(), the runtime should be in the same state as before load() was called.

🔎 Suggested fix for consistent cleanup
 public stop(): void
 {
+    this.scriptCode = null;
     this.vm = null;
     this.sandbox = null;
     this.runningSince = null;

     if (null !== this.logWriter) {
         this.logWriter.close();
+        this.logWriter = null;
     }

     this.eventEmitter.emit(AutomationEventType.scriptStopped);
     console.log('script stopped')
 }

47-78: Potential resource leak if load() is called multiple times.

The load() method creates new resources (VMScript, NodeVM, WriteStream) without cleaning up existing ones. If load() is called again without calling stop() first, the old resources will be orphaned, particularly the WriteStream file handle.

Consider adding a guard or calling cleanup logic at the start of load() to prevent resource leaks.

🔎 Suggested fix to prevent resource leaks
 public load(scriptCode: string): void
 {
+    // Clean up existing resources before loading new ones
+    if (null !== this.logWriter) {
+        this.logWriter.close();
+    }
+    
     this.scriptCode = new VMScript(scriptCode);

     this.sandbox = {
         event: { type: null, device: null },
         devices: this.deviceRepository,
         context: {},
     }

     this.vm = new NodeVM({
         console: 'redirect',
         require: {
             external: true,
             root: './',
         },
         sandbox: this.sandbox
     });

     this.logWriter = fs.createWriteStream(`${this.logPath}/automation.log`)

     this.vm.on('console.log', (data: string) => {
         console.log(`VM stdout: ${data}`);
         void this.log(data);
         this.eventEmitter.emit(AutomationEventType.consoleLog, data);
     });

     this.runningSince = new Date();

     this.eventEmitter.emit(AutomationEventType.scriptStarted);
     console.log('script loaded')
 }
🧹 Nitpick comments (19)
src/schemaValidation/JsonSchemaValidator.ts (1)

23-25: Consider applying the same pattern for consistency.

For consistency with line 20, you might consider using the nullish coalescing operator here as well, though ajv.errorsText() typically handles undefined gracefully.

🔎 Optional consistency improvement
 public getValidationErrorsAsText(): string {
-    return this.ajv.errorsText(this.schemaValidator.errors);
+    return this.ajv.errorsText(this.schemaValidator.errors ?? []);
 }
src/controller/automation/runScriptController.ts (1)

16-21: Content-Type validation logic is correct.

The explicit check improves type safety for strict mode. The logic correctly rejects requests when the content type doesn't match.

💡 Optional: Simplify the condition

The condition could be more concise while maintaining the same behavior:

-        const matchedContentType = req.is('text/plain');
-
-        if(false === matchedContentType || null === matchedContentType) {
+        if(!req.is('text/plain')) {
             res.status(400).send('Content-Type header must be text/plain');
             return;
         }

Since req.is() returns string | false | null, the negation !req.is(...) will be true for both false and null, and false for any matching string.

src/util/objects.ts (1)

3-5: LGTM! Idiomatic typed key accessor.

This follows the standard TypeScript pattern for obtaining strongly-typed object keys. The implementation correctly wraps Object.keys() with a type assertion to provide better type safety for downstream consumers, which aligns well with the strict mode typing goals mentioned in the commits.

Optional: Consider adding JSDoc for API clarity

Since this is a utility function that will be used across the codebase, brief JSDoc documentation could help other developers understand its purpose:

+/**
+ * Returns the keys of an object with proper TypeScript typing.
+ * Only includes string keys (excludes symbols).
+ */
 export const getTypedKeys = <T extends object>(obj: T): (StringKey<T>)[] => {
     return Object.keys(obj) as StringKey<T>[];
 }
src/controller/automation/createScriptController.ts (1)

17-22: LGTM! Explicit type validation improves clarity.

The intermediate variable and explicit checks for both false and null make the validation logic clearer and align well with the PR's stricter typing approach. This correctly handles both content-type mismatch and missing body cases.

Optional style refinements

If desired, consider these minor style adjustments:

-        const matchedContentType = req.is('text/plain');
-
-        if(false === matchedContentType || null === matchedContentType) {
+        const matchedContentType = req.is('text/plain');
+
+        if (matchedContentType === false || matchedContentType === null) {
             res.status(400).send('Content-Type header must be text/plain');
             return;
         }
  • Add space after if keyword
  • Use conventional comparison order (subject-verb-object vs. Yoda style)

These are purely stylistic and don't affect functionality.

src/serial/SynchronousSerialPort.ts (2)

45-46: Simplify eslint-disable comment.

The comment includes both the TypeScript ESLint rule name and the base ESLint rule name. Since you're using TypeScript, only the @typescript-eslint/no-empty-function rule is necessary.

🔎 Proposed simplification
-        // eslint-disable-next-line @typescript-eslint/no-empty-function,no-empty-function
+        // eslint-disable-next-line @typescript-eslint/no-empty-function
         let removeListeners: () => void = () => {};

54-59: Remove redundant check.

The check if (undefined !== removeListeners) is now redundant since removeListeners is always initialized to a function (either the no-op on line 46 or the cleanup function on line 61). The function is guaranteed to be defined at this point.

🔎 Proposed fix
 const dataHandler = (receivedData: string): void => {
-    if (undefined !== removeListeners) {
-        removeListeners();
-    }
+    removeListeners();
     resolve(receivedData);
 };
src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts (1)

42-43: Minor: URL is constructed twice.

The WebSocket URL is built identically on lines 42 and 73. Consider extracting to a property or reusing the value.

Also applies to: 73-73

src/device/attribute/numberDeviceAttribute.ts (1)

5-5: Clarify the NumberAttributeValue type definition.

The type NotJustUndefined<Int | Float | undefined> appears contradictory: NotJustUndefined is intended to exclude undefined, yet undefined is explicitly part of the union Int | Float | undefined. This results in NumberAttributeValue resolving to Int | Float (with undefined stripped), which may be the intended behavior but is not immediately clear.

Consider simplifying to make the intent explicit:

🔎 Proposed simplification
-export type NumberAttributeValue = NotJustUndefined<Int | Float | undefined>;
+export type NumberAttributeValue = Int | Float;

Alternatively, if optional numeric values are needed in some contexts, document why NotJustUndefined is applied here.

src/device/protocol/zc95/zc95DeviceFactory.ts (1)

66-79: Minor formatting: missing space after comma.

On line 68, there's a missing space after the comma between 'Pattern' and DeviceAttributeModifier.readWrite.

Otherwise, the migration to the new attribute system with ListDeviceAttribute and BoolDeviceAttribute factories is correct, and the return shape properly aligns with the Zc95DeviceAttributes type.

🔎 Formatting fix
-        'activePattern', 'Pattern',DeviceAttributeModifier.readWrite, patterns, Int.ZERO
+        'activePattern', 'Pattern', DeviceAttributeModifier.readWrite, patterns, Int.ZERO
src/device/attribute/intRangeDeviceAttribute.ts (2)

72-80: Consider extracting shared parsing logic.

The fromString implementation duplicates the parsing logic from IntDeviceAttribute. Consider extracting this to a shared utility or having IntRangeDeviceAttribute delegate to a common parsing function to reduce duplication.

Possible approach

Create a shared utility in intDeviceAttribute.ts:

export function parseIntValue(value: string, context: string): Int {
    const res = parseInt(value, 10);
    if (isNaN(res)) {
        throw new Error(`Could not convert '${value}' to a valid value for ${context}`);
    }
    return res as Int;
}

Then use it in both classes:

 public fromString(value: string): T {
-    const res = parseInt(value, 10);
-
-    if (isNaN(res)) {
-        throw new Error(`Could not convert '${value}' to a valid value for ${this.constructor.name}`);
-    }
-
-    return res as T;
+    return parseIntValue(value, this.constructor.name) as T;
 }

19-80: Consider adding range validation for values.

Neither the constructor nor fromString validates that values fall within [min, max]. This could allow invalid values to be set or parsed. Consider adding validation:

🔎 Proposed validation

Add a private validation method:

private validateRange(value: Int): void {
    if (value < this._min || value > this._max) {
        throw new Error(
            `Value ${value} is out of range [${this._min}, ${this._max}] for ${this.name}`
        );
    }
}

Then call it in the constructor (if initialValue is not undefined) and in fromString:

 public constructor(name: string, label: string | undefined, modifier: DeviceAttributeModifier, uom: string | undefined, min: Int, max: Int, incrementStep: Int, initialValue: T) {
     super(name, label, modifier, uom, initialValue);
     this._min = min;
     this._max = max;
     this._incrementStep = incrementStep;
+    if (initialValue !== undefined) {
+        this.validateRange(initialValue as Int);
+    }
 }
 public fromString(value: string): T {
     const res = parseInt(value, 10);

     if (isNaN(res)) {
         throw new Error(`Could not convert '${value}' to a valid value for ${this.constructor.name}`);
     }

+    this.validateRange(res as Int);
     return res as T;
 }
src/device/protocol/virtual/randomGenerator/randomGeneratorVirtualDeviceLogic.ts (1)

17-26: Consider using Object.hasOwn() instead of hasOwnProperty.

hasOwnProperty can be shadowed if the object has a property with that name. Modern TypeScript prefers Object.hasOwn() or the in operator.

🔎 Proposed fix
 public constructor(config: JsonObject) {
-    if (!config.hasOwnProperty('min')) {
+    if (!Object.hasOwn(config, 'min')) {
         throw new Error(`Config value 'min' missing`);
     }
-    if (!config.hasOwnProperty('max')) {
+    if (!Object.hasOwn(config, 'max')) {
         throw new Error(`Config value 'max' missing`);
     }
     this.min = config.min as number;
     this.max = config.max as number;
 }
src/device/protocol/virtual/virtualDevice.ts (1)

47-66: Consider simplifying to async/await pattern.

The explicit new Promise with reject is verbose. Since this is already an async method, you could use throw directly. However, the current implementation is functionally correct.

🔎 Proposed simplification
-    public async setAttribute<K extends keyof T, V extends AttributeValue<T[K]>>(attributeName: K, value: V): Promise<V> {
-        return new Promise<V>((resolve, reject) => {
-            this.state = DeviceState.busy;
-
-            const attribute = this.attributes[attributeName];
-
-            if (undefined === attribute) {
-                reject(new Error(
-                    `Attribute named "${attributeName.toString()}" does not exist for device with id "${this.deviceId}"`
-                ));
-                return;
-            }
-
-            attribute.value = value;
-
-            this.state = DeviceState.ready;
-
-            resolve(value);
-        });
-    }
+    public async setAttribute<K extends keyof T, V extends AttributeValue<T[K]>>(attributeName: K, value: V): Promise<V> {
+        this.state = DeviceState.busy;
+
+        const attribute = this.attributes[attributeName];
+
+        if (undefined === attribute) {
+            this.state = DeviceState.ready;
+            throw new Error(
+                `Attribute named "${attributeName.toString()}" does not exist for device with id "${this.deviceId}"`
+            );
+        }
+
+        attribute.value = value;
+        this.state = DeviceState.ready;
+
+        return value;
+    }
src/device/attribute/deviceAttribute.ts (1)

14-17: Clarify the purpose of the type field.

The type field is always undefined and marked as "only here to expose it explicitly." Consider adding a brief doc comment explaining how subclasses should set this (if at all) or whether this is for serialization discrimination purposes.

🔎 Suggested documentation
 @Exclude()
 export default abstract class DeviceAttribute<T extends AttributeValue = AttributeValue> {
+    /**
+     * Discriminator field for serialization. Subclasses should override
+     * this value for proper type identification during deserialization.
+     */
     @Expose()
     private readonly type: string|undefined = undefined; // This field is only here to expose it explicitly
src/device/attribute/listDeviceAttribute.ts (2)

54-57: Consider validating parsed values against the available keys.

The fromString method parses the input but doesn't verify that the result exists in _values. This could allow invalid values if callers don't separately validate using isValidValue.

Consider adding validation or documenting that callers must validate the result:

🔎 Suggested improvement
 public fromString(value: string): V {
     const parsedInt = parseInt(value, 10);
-    return (isNaN(parsedInt) ? value : parsedInt) as V;
+    const result = (isNaN(parsedInt) ? value : parsedInt) as V;
+    if (!this.isValidValue(result)) {
+        throw new Error(`Value '${value}' is not a valid key for attribute '${this.name}'`);
+    }
+    return result;
 }

Alternatively, document that callers must validate using isValidValue after calling fromString.


67-72: Type safety consideration: Int validation.

The isValidValue method checks typeof value === "number", but IKey extends string|Int where Int is a branded type representing integers. This allows any number to pass the type check, not just integers.

While this may work at runtime since Map will match the key, consider whether strict integer validation is needed:

public isValidValue(value: unknown): value is NotUndefined<V> {
    if (typeof value === "string") {
        return this._values.has(value as IKey);
    }
    if (typeof value === "number") {
        if (!Number.isInteger(value)) {
            return false;
        }
        return this._values.has(value as IKey);
    }
    return false;
}

This ensures only integers (not floats) are accepted when IKey is Int.

src/device/protocol/buttplugIo/buttplugIoDevice.ts (1)

75-77: Validate attribute name parsing.

The attribute name is split and parsed without validation. If the attribute name doesn't match the expected format ${ActuatorType}-${number}, this could lead to runtime errors.

Consider adding validation:

🔎 Suggested improvement
 const [actuatorType, index] = attributeName.split('-');
+
+if (!actuatorType || !index) {
+    throw new Error(`Invalid attribute name format: ${attributeName}`);
+}
+
+const parsedIndex = parseInt(index, 10);
+if (isNaN(parsedIndex)) {
+    throw new Error(`Invalid index in attribute name: ${attributeName}`);
+}

-await this.send(actuatorType as ActuatorType, parseInt(index, 10), valueToSend);
+await this.send(actuatorType as ActuatorType, parsedIndex, valueToSend);
src/device/protocol/slvCtrlPlus/slvCtrlPlusMessageParser.ts (1)

106-115: Consider validation for range parsing.

Lines 112-113 use parseInt without checking for NaN. If the regex matches but the values aren't valid integers, Int.from() will throw an error.

While the regex (\d+)-(\d+) should ensure valid integers, consider adding explicit validation for robustness:

🔎 Suggested improvement
 } else if (null !== (result = reRange.exec(value))) {
+    const min = parseInt(result[1], 10);
+    const max = parseInt(result[2], 10);
+    if (isNaN(min) || isNaN(max)) {
+        throw new Error(`Invalid range values in attribute definition: ${value}`);
+    }
     attr = IntRangeDeviceAttribute.create(
         name,
         undefined,
         modifier,
         undefined,
-        Int.from(parseInt(result[1], 10)),
-        Int.from(parseInt(result[2], 10)),
+        Int.from(min),
+        Int.from(max),
         Int.from(1),
     );
src/device/protocol/zc95/zc95Device.ts (1)

255-288: Redundant value assignment after initialization.

Line 284 sets the attribute value to menuItem.Default, but this was already done during initialization with createInitialized (lines 264 and 275). The createInitialized factory methods accept initialValue as the last parameter and set it during construction.

Consider removing line 284:

🔎 Suggested cleanup
             patternAttributes[attrName as keyof Zc95DevicePatternAttributes] = ListDeviceAttribute.createInitialized<Int, string>(
                 attrName,
                 menuItem.Title,
                 DeviceAttributeModifier.readWrite,
                 new Map(menuItem.Choices.map((choice) => [Int.from(choice.Id), choice.Name])),
                 Int.from(menuItem.Default),
             );
         }
-
-        patternAttributes[`${Zc95Device.patternAttributePrefix}${menuItem.Id}`].value = Int.from(menuItem.Default);
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5787c61 and 0d6fd39.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (62)
  • .eslintrc.cjs
  • package.json
  • src/automation/scriptRuntime.ts
  • src/controller/automation/createScriptController.ts
  • src/controller/automation/runScriptController.ts
  • src/device/attribute/boolDeviceAttribute.ts
  • src/device/attribute/boolGenericDeviceAttribute.ts
  • src/device/attribute/deviceAttribute.ts
  • src/device/attribute/floatDeviceAttribute.ts
  • src/device/attribute/floatGenericDeviceAttribute.ts
  • src/device/attribute/genericDeviceAttribute.ts
  • src/device/attribute/intDeviceAttribute.ts
  • src/device/attribute/intGenericDeviceAttribute.ts
  • src/device/attribute/intRangeDeviceAttribute.ts
  • src/device/attribute/listDeviceAttribute.ts
  • src/device/attribute/listGenericDeviceAttribute.ts
  • src/device/attribute/numberDeviceAttribute.ts
  • src/device/attribute/rangeGenericDeviceAttribute.ts
  • src/device/attribute/strDeviceAttribute.ts
  • src/device/attribute/strGenericDeviceAttribute.ts
  • src/device/device.ts
  • src/device/genericDeviceUpdater.ts
  • src/device/protocol/buttplugIo/buttplugIoDevice.ts
  • src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts
  • src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts
  • src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProviderFactory.ts
  • src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts
  • src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts
  • src/device/protocol/slvCtrlPlus/slvCtrlPlusDeviceFactory.ts
  • src/device/protocol/slvCtrlPlus/slvCtrlPlusMessageParser.ts
  • src/device/protocol/slvCtrlPlus/slvCtrlPlusSerialDeviceProvider.ts
  • src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts
  • src/device/protocol/virtual/delegatedVirtualDeviceFactory.ts
  • src/device/protocol/virtual/display/displayVirtualDeviceLogic.ts
  • src/device/protocol/virtual/randomGenerator/randomGeneratorVirtualDeviceLogic.ts
  • src/device/protocol/virtual/virtualDevice.ts
  • src/device/protocol/virtual/virtualDeviceProvider.ts
  • src/device/protocol/zc95/Zc95Serial.ts
  • src/device/protocol/zc95/zc95Device.ts
  • src/device/protocol/zc95/zc95DeviceFactory.ts
  • src/device/protocol/zc95/zc95SerialDeviceProvider.ts
  • src/device/provider/deviceProviderLoader.ts
  • src/device/transport/serialDeviceTransport.ts
  • src/index.ts
  • src/logging/PinoLogger.ts
  • src/schemaValidation/JsonSchemaValidator.ts
  • src/serial/SynchronousSerialPort.ts
  • src/serialization/classToPlainSerializer.ts
  • src/serialization/discriminator/genericDeviceAttributeDiscriminator.ts
  • src/settings/deviceSource.ts
  • src/settings/knownDevice.ts
  • src/settings/settings.ts
  • src/settings/settingsManager.ts
  • src/util/numbers.ts
  • src/util/objects.ts
  • src/version.ts
  • tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts
  • tests/unit/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.spec.ts
  • tests/unit/device/protocol/slvCtrlPlus/slvCtrlPlusMessageParser.spec.ts
  • tests/unit/device/protocol/virtual/display/displayVirtualDevice.spec.ts
  • tests/unit/device/testDevice.ts
  • tsconfig.json
💤 Files with no reviewable changes (9)
  • src/device/attribute/floatGenericDeviceAttribute.ts
  • src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProviderFactory.ts
  • src/device/attribute/boolGenericDeviceAttribute.ts
  • src/device/attribute/rangeGenericDeviceAttribute.ts
  • package.json
  • src/device/attribute/genericDeviceAttribute.ts
  • src/device/attribute/intGenericDeviceAttribute.ts
  • src/device/attribute/strGenericDeviceAttribute.ts
  • src/device/attribute/listGenericDeviceAttribute.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/serialization/classToPlainSerializer.ts
  • src/device/protocol/virtual/display/displayVirtualDeviceLogic.ts
  • tests/unit/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.spec.ts
  • tests/unit/device/protocol/virtual/display/displayVirtualDevice.spec.ts
  • src/device/protocol/virtual/virtualDeviceProvider.ts
  • src/index.ts
🧰 Additional context used
🧬 Code graph analysis (27)
src/logging/PinoLogger.ts (1)
src/logging/Logger.ts (1)
  • Logger (1-11)
src/device/protocol/zc95/Zc95Serial.ts (1)
src/device/protocol/zc95/Zc95Messages.ts (1)
  • MsgResponse (4-10)
src/device/genericDeviceUpdater.ts (2)
src/device/device.ts (1)
  • DeviceData (11-14)
src/util/objects.ts (1)
  • getTypedKeys (3-5)
src/device/attribute/floatDeviceAttribute.ts (3)
src/device/attribute/deviceAttribute.ts (3)
  • NotJustUndefined (4-4)
  • value (53-55)
  • value (57-59)
src/util/numbers.ts (2)
  • Float (17-17)
  • Float (20-29)
src/device/attribute/numberDeviceAttribute.ts (1)
  • uom (23-25)
src/controller/automation/createScriptController.ts (1)
src/middleware/contentTypeMiddleware.ts (1)
  • req (3-18)
src/device/attribute/intDeviceAttribute.ts (4)
src/device/attribute/deviceAttribute.ts (4)
  • NotJustUndefined (4-4)
  • name (38-40)
  • value (53-55)
  • value (57-59)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/settings/knownDevice.ts (1)
  • name (43-45)
src/device/attribute/numberDeviceAttribute.ts (1)
  • uom (23-25)
src/device/protocol/virtual/virtualDevice.ts (2)
src/device/protocol/virtual/virtualDeviceLogic.ts (1)
  • VirtualDeviceLogic (4-11)
src/device/attribute/deviceAttribute.ts (3)
  • AttributeValue (6-6)
  • value (53-55)
  • value (57-59)
src/device/protocol/virtual/randomGenerator/randomGeneratorVirtualDeviceLogic.ts (3)
src/device/attribute/intDeviceAttribute.ts (1)
  • IntDeviceAttribute (8-38)
src/device/protocol/virtual/virtualDeviceLogic.ts (1)
  • VirtualDeviceLogic (4-11)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/serialization/discriminator/genericDeviceAttributeDiscriminator.ts (6)
src/device/attribute/boolDeviceAttribute.ts (1)
  • BoolDeviceAttribute (6-32)
src/device/attribute/intDeviceAttribute.ts (1)
  • IntDeviceAttribute (8-38)
src/device/attribute/floatDeviceAttribute.ts (1)
  • FloatDeviceAttribute (8-48)
src/device/attribute/strDeviceAttribute.ts (1)
  • StrDeviceAttribute (6-32)
src/device/attribute/intRangeDeviceAttribute.ts (1)
  • IntRangeDeviceAttribute (9-81)
src/device/attribute/listDeviceAttribute.ts (1)
  • ListDeviceAttribute (11-73)
src/device/attribute/intRangeDeviceAttribute.ts (5)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/device/attribute/intDeviceAttribute.ts (1)
  • IntAttributeValue (5-5)
src/device/attribute/deviceAttribute.ts (5)
  • name (38-40)
  • label (42-44)
  • modifier (46-48)
  • value (53-55)
  • value (57-59)
src/settings/knownDevice.ts (1)
  • name (43-45)
src/device/attribute/numberDeviceAttribute.ts (1)
  • uom (23-25)
src/device/protocol/slvCtrlPlus/slvCtrlPlusDeviceFactory.ts (1)
src/settings/knownDevice.ts (1)
  • serialNo (39-41)
src/device/protocol/slvCtrlPlus/slvCtrlPlusMessageParser.ts (10)
src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts (1)
  • SlvCtrlPlusDeviceAttributes (7-7)
src/device/attribute/deviceAttribute.ts (4)
  • value (53-55)
  • value (57-59)
  • name (38-40)
  • modifier (46-48)
src/settings/knownDevice.ts (2)
  • name (43-45)
  • type (47-49)
src/device/attribute/boolDeviceAttribute.ts (1)
  • BoolDeviceAttribute (6-32)
src/device/attribute/intDeviceAttribute.ts (1)
  • IntDeviceAttribute (8-38)
src/device/attribute/floatDeviceAttribute.ts (1)
  • FloatDeviceAttribute (8-48)
src/device/attribute/strDeviceAttribute.ts (1)
  • StrDeviceAttribute (6-32)
src/device/attribute/intRangeDeviceAttribute.ts (1)
  • IntRangeDeviceAttribute (9-81)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/device/attribute/listDeviceAttribute.ts (1)
  • ListDeviceAttribute (11-73)
tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts (4)
src/device/protocol/buttplugIo/buttplugIoDevice.ts (2)
  • ButtplugIoDeviceAttributes (14-17)
  • ButtplugIoDeviceAttributeKey (12-12)
src/device/attribute/boolDeviceAttribute.ts (1)
  • BoolDeviceAttribute (6-32)
src/device/attribute/intRangeDeviceAttribute.ts (1)
  • IntRangeDeviceAttribute (9-81)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/settings/knownDevice.ts (2)
src/settings/deviceSource.ts (3)
  • id (21-23)
  • type (25-27)
  • config (29-31)
src/device/attribute/deviceAttribute.ts (1)
  • name (38-40)
src/device/protocol/zc95/zc95DeviceFactory.ts (4)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/device/protocol/zc95/zc95Device.ts (1)
  • Zc95DeviceAttributes (34-35)
src/device/attribute/listDeviceAttribute.ts (1)
  • ListDeviceAttribute (11-73)
src/device/attribute/boolDeviceAttribute.ts (1)
  • BoolDeviceAttribute (6-32)
src/device/protocol/zc95/zc95Device.ts (7)
src/device/attribute/listDeviceAttribute.ts (2)
  • InitializedListDeviceAttribute (6-9)
  • ListDeviceAttribute (11-73)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/device/attribute/boolDeviceAttribute.ts (1)
  • InitializedBoolDeviceAttribute (4-4)
src/device/attribute/intRangeDeviceAttribute.ts (2)
  • IntRangeDeviceAttribute (9-81)
  • InitializedIntRangeDeviceAttribute (7-7)
src/device/device.ts (1)
  • AttributeValue (8-8)
src/util/objects.ts (1)
  • getTypedKeys (3-5)
src/device/protocol/zc95/Zc95Messages.ts (2)
  • MinMaxMenuItem (46-51)
  • MultiChoiceMenuItem (53-55)
src/device/attribute/listDeviceAttribute.ts (2)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/device/attribute/deviceAttribute.ts (6)
  • name (38-40)
  • label (42-44)
  • modifier (46-48)
  • value (53-55)
  • value (57-59)
  • NotUndefined (5-5)
src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts (5)
src/device/protocol/buttplugIo/buttplugIoDevice.ts (2)
  • ButtplugIoDeviceAttributes (14-17)
  • ButtplugIoDeviceAttributeKey (12-12)
src/device/attribute/intRangeDeviceAttribute.ts (1)
  • IntRangeDeviceAttribute (9-81)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/device/attribute/boolDeviceAttribute.ts (1)
  • BoolDeviceAttribute (6-32)
src/device/attribute/intDeviceAttribute.ts (1)
  • IntDeviceAttribute (8-38)
src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts (1)
src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts (1)
  • Exclude (9-118)
src/settings/deviceSource.ts (1)
src/settings/knownDevice.ts (3)
  • id (35-37)
  • type (47-49)
  • config (55-57)
src/controller/automation/runScriptController.ts (1)
src/middleware/contentTypeMiddleware.ts (1)
  • req (3-18)
src/device/attribute/strDeviceAttribute.ts (1)
src/device/attribute/deviceAttribute.ts (7)
  • NotJustUndefined (4-4)
  • name (38-40)
  • label (42-44)
  • modifier (46-48)
  • value (53-55)
  • value (57-59)
  • NotUndefined (5-5)
src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts (4)
src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts (1)
  • SlvCtrlPlusDeviceAttributes (7-7)
src/device/attribute/deviceAttribute.ts (3)
  • AttributeValue (6-6)
  • value (53-55)
  • value (57-59)
src/device/device.ts (2)
  • AttributeValue (8-8)
  • DeviceAttributes (6-6)
src/device/attribute/boolDeviceAttribute.ts (1)
  • BoolDeviceAttribute (6-32)
src/device/device.ts (1)
src/device/attribute/deviceAttribute.ts (3)
  • AttributeValue (6-6)
  • value (53-55)
  • value (57-59)
src/automation/scriptRuntime.ts (1)
src/repository/deviceRepositoryInterface.ts (1)
  • DeviceRepositoryInterface (3-8)
src/device/protocol/virtual/delegatedVirtualDeviceFactory.ts (1)
src/device/protocol/virtual/virtualDeviceFactory.ts (1)
  • VirtualDeviceFactory (4-9)
tests/unit/device/testDevice.ts (1)
src/device/device.ts (2)
  • DeviceAttributes (6-6)
  • AttributeValue (8-8)
🔇 Additional comments (72)
src/schemaValidation/JsonSchemaValidator.ts (1)

19-21: LGTM! Good defensive programming.

The nullish coalescing operator ensures the method always returns an array, preventing potential runtime errors when callers expect array methods to be available.

src/util/objects.ts (1)

1-1: LGTM! Clean type helper.

The StringKey<T> helper correctly extracts string keys from an object type, properly filtering out symbols and numbers.

.eslintrc.cjs (3)

106-106: Excellent addition for type safety.

The @typescript-eslint/strict-boolean-expressions rule prevents implicit truthiness checks on non-boolean types, requiring explicit comparisons (e.g., value !== null instead of if (value)). This aligns perfectly with the PR's goal of enabling strict mode and full typing.


152-157: Good adjustment for TypeScript conventions.

Relaxing no-underscore-dangle to allow this._property accommodates the common TypeScript pattern for private-like fields, which aligns with idiomatic TypeScript code while still preventing underscore dangles in other contexts.


162-166: Enforces explicit type conversions for clarity.

The no-implicit-coercion rule prevents shortcuts like !!value or +value, requiring explicit Boolean(), Number(), and String() calls. While this adds some verbosity, it improves code clarity and prevents subtle coercion bugs, which is consistent with the strict typing approach in this PR.

src/serial/SynchronousSerialPort.ts (1)

84-84: Manual verification required for type cast in queue.push() return type.

The cast as Promise<string> cannot be verified automatically due to repository access issues. Verify that SequentialTaskQueue.push() return type aligns with Promise<string> when the callback returns Promise<string>, and confirm whether the type assertion is necessary or can be removed.

src/automation/scriptRuntime.ts (3)

27-40: LGTM! Nullable field declarations align with strict typing.

The nullable declarations properly represent the ScriptRuntime's lifecycle states (unloaded vs. loaded) and align with the PR's strict TypeScript mode initiative. The explicit null initialization and subsequent assignment in load() make the state transitions clear.


94-126: LGTM! Proper guards and return type alignment.

The execution guard in runForEvent() correctly verifies all required runtime resources (vm, sandbox, scriptCode) are non-null before executing, preventing crashes if called before load() or after stop().

The getRunningSince() return type change to Date|null properly aligns with the nullable field declaration.


128-133: LGTM! Defensive guard for null logWriter.

The null check before writing prevents crashes if log() is called before load() or after stop(). The silent no-op behavior is appropriate for a logging method.

src/device/protocol/slvCtrlPlus/slvCtrlPlusSerialDeviceProvider.ts (1)

102-109: LGTM!

The type correction from Error to Error|null properly reflects the SerialPort callback signature where err is null on successful open. The existing null-check logic handles this correctly.

src/serialization/discriminator/genericDeviceAttributeDiscriminator.ts (1)

1-17: LGTM!

The imports and discriminator map are correctly updated to reference the new attribute classes while preserving the same serialization keys for backward compatibility.

src/util/numbers.ts (1)

1-29: LGTM!

The branded numeric types (Int and Float) are well-implemented using TypeScript's nominal typing pattern. The runtime validation in from() methods correctly enforces constraints:

  • Int.from: Rejects non-integers via Number.isInteger
  • Float.from: Rejects NaN and infinite values via isNaN and Number.isFinite

The merged type-and-const pattern with eslint disables is an accepted idiom for this use case.

src/device/attribute/boolDeviceAttribute.ts (1)

25-27: Verify fromString parsing logic matches protocol expectations.

The current implementation treats only '1' as true and everything else (including 'true', 'TRUE', 'yes') as false. This may be intentional for a specific wire protocol, but could be surprising if string-based boolean inputs vary.

src/device/protocol/zc95/Zc95Serial.ts (1)

16-16: LGTM!

Initializing pendingRecvMessage to null is more explicit than leaving it undefined and aligns with the recv() return type of Promise<MsgResponse | null>.

src/device/protocol/zc95/zc95SerialDeviceProvider.ts (1)

65-68: LGTM!

Good defensive check that fails fast with a clear error message rather than allowing a potential undefined access on versionDetails.ZC95. The error is properly caught by the surrounding try-catch block.

src/device/provider/deviceProviderLoader.ts (1)

41-46: LGTM!

Good simplification - consolidating the has() + get() pattern into a single get() with an undefined check eliminates the redundant map lookup.

tsconfig.json (1)

11-11: LGTM! Excellent type safety improvement.

Enabling strict mode is a best practice that catches many potential runtime errors at compile time. This aligns well with the PR's objective of introducing a fully-typed attribute system.

src/device/protocol/slvCtrlPlus/slvCtrlPlusDeviceFactory.ts (1)

63-80: LGTM! Clean refactor to constructor-based initialization.

The change from null-checking to undefined-checking aligns with strict mode, and using the parameterized KnownDevice constructor improves immutability and encapsulation. The logic is preserved correctly.

src/device/protocol/virtual/delegatedVirtualDeviceFactory.ts (1)

3-3: LGTM! Improved type specificity and cleaner retrieval pattern.

The return type is now more specific (VirtualDevice instead of Device), and the factory retrieval pattern is simplified by eliminating the redundant has() check. The error handling is preserved.

Also applies to: 9-23

src/settings/settingsManager.ts (3)

96-98: LGTM! Good defensive programming.

The early return guard prevents attempting to save undefined settings, avoiding potential errors. This aligns well with the optional settings field change.


118-122: LGTM! Improved encapsulation with constructor usage.

Using the DeviceSource constructor instead of stepwise property assignment improves immutability and aligns with the encapsulation improvements across the codebase.


91-93: Unable to verify caller handling of potentially undefined return value from getSettings().

The review comment identifies that getSettings() now returns Settings|undefined instead of Settings, which is a breaking API change. However, verification of whether all callers properly handle the undefined case could not be completed due to repository access limitations.

src/settings/deviceSource.ts (1)

6-31: LGTM! Excellent encapsulation with proper serialization mapping.

The refactor from public fields to private readonly fields with getters improves encapsulation and immutability. The @Expose decorators correctly maintain serialization compatibility with the original field names, ensuring backward compatibility with persisted settings.

src/device/attribute/numberDeviceAttribute.ts (1)

27-29: The isValidValue check doesn't enforce branded Int or Float types.

The validation typeof value === 'number' accepts any JavaScript number, including those not explicitly branded as Int or Float. While this may be acceptable at runtime (since branded types are erased), it means the type guard doesn't enforce the stricter compile-time contract.

If stricter validation is needed (e.g., checking that integers don't have fractional parts), consider adding runtime checks. Otherwise, this is acceptable.

src/settings/settings.ts (1)

11-20: LGTM! Constructor initialization and consistent use of undefined.

The explicit constructor properly initializes both maps, and the migration from null to undefined for the return type of getKnownDeviceById aligns with TypeScript best practices.

tests/unit/device/testDevice.ts (1)

1-25: LGTM! Test device correctly migrated to map-based attribute model.

The changes properly align with the new typed attribute system:

  • Empty object {} replaces the empty array for attributes
  • Generic setAttribute signature correctly constrains keys and values using DeviceAttributes and AttributeValue
src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts (1)

4-25: LGTM! Generic attribute typing enhances type safety.

The introduction of generic type parameter A extends SlvCtrlPlusDeviceAttributes provides strong typing for device attributes while maintaining flexibility. The type aliases SlvCtrlPlusDeviceAttributeKey and SlvCtrlPlusDeviceAttributes clearly document the attribute structure.

src/device/attribute/strDeviceAttribute.ts (1)

1-32: LGTM! String attribute implementation is clean and follows established patterns.

The factory methods (createInitialized and create) provide clear initialization patterns, and the fromString and isValidValue implementations are appropriate for string attributes.

src/device/attribute/intDeviceAttribute.ts (1)

1-38: LGTM! Integer attribute implementation with proper parsing validation.

The fromString method correctly uses parseInt with radix 10 and throws a descriptive error on invalid input. The use of this.constructor.name in the error message aids debugging.

src/device/protocol/virtual/randomGenerator/randomGeneratorVirtualDeviceLogic.ts (2)

7-11: Well-structured typed attribute definition.

The RandomGeneratorVirtualDeviceAttributes type provides clear typing for the device's attributes, aligning with the broader PR goal of uniting attribute values into a typed key-value structure.


32-45: Correct use of Int.from() for type-safe integer values.

The refreshData method properly wraps the random number with Int.from() to satisfy the IntDeviceAttribute type requirements. The configureAttributes method returns a properly structured object matching RandomGeneratorVirtualDeviceAttributes.

src/device/protocol/virtual/virtualDevice.ts (1)

2-7: Good generic typing for flexible device attributes.

The generic type parameter T extends DeviceAttributes = DeviceAttributes properly constrains the attribute types while providing a sensible default. The import from "../../device.js" correctly includes the .js extension for ESM compatibility.

tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts (3)

27-40: Good test coverage for non-existing attribute error handling.

The test properly verifies that setting a non-existent attribute throws the expected error with the attribute name in the message.


42-63: Test correctly uses the new attribute API.

The test properly creates a BoolDeviceAttribute using the factory method and accesses the value via the .value property after getAttribute().


65-100: Comprehensive test for range attribute handling.

The test correctly:

  • Creates an IntRangeDeviceAttribute with proper Int typed bounds
  • Uses Int.from() for the new value
  • Verifies the scalar calculation uses newValue/rangeAttr.max
src/device/attribute/deviceAttribute.ts (3)

4-6: Well-designed utility types for attribute value constraints.

NotJustUndefined<V> prevents V from being only undefined, while NotUndefined<V> excludes undefined from a union. This ensures attributes always have a meaningful value type while allowing undefined as one option in a union.


61-63: Type guard implementation is correct.

The hasValue() method properly narrows the type. Since T could include undefined in the union (per AttributeValue definition), checking !== undefined correctly identifies when a concrete value exists.


69-71: Static isInstance type guard is well-implemented.

The pattern using this: new (...args: any[]) => U allows subclasses to call BoolDeviceAttribute.isInstance(attr) and get proper type narrowing to BoolDeviceAttribute.

tests/unit/device/protocol/slvCtrlPlus/slvCtrlPlusMessageParser.spec.ts (2)

21-53: Tests properly verify the new dictionary-based attribute structure.

The assertions correctly:

  • Use Object.keys(result).length to count attributes
  • Access attributes by key (e.g., result.connected, result.mode)
  • Verify instanceof for correct attribute class types
  • Cast to specific types when accessing subclass properties like min/max

77-106: Edge case tests properly updated for new return type.

The tests for empty attributes, malformed attributes, and empty status correctly use Object.keys(result).length to verify the attribute count in the dictionary structure.

src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts (2)

46-63: Proper handling of empty attribute values.

The use of undefined for empty strings (line 62) aligns with the AttributeValue type definition which includes undefined. This addresses previous concerns about assigning null to attribute values.


87-94: Good use of type guard for boolean handling.

Using BoolDeviceAttribute.isInstance(attr) for runtime type checking and converting booleans to 0/1 for the SlvCtrl protocol is a clean pattern.

src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts (4)

59-77: Correct attribute creation for ScalarCmd items.

The factory properly:

  • Creates composite keys from ActuatorType-Index
  • Uses IntRangeDeviceAttribute.createInitialized for step counts > 2
  • Uses BoolDeviceAttribute.createInitialized for binary values
  • Applies Int.from() and Int.ZERO for type-safe integer bounds

80-105: Good fallback logic for SensorReadCmd attributes.

The conditional check for StepRange.length === 2 with a fallback to IntDeviceAttribute is a reasonable approach when the range specification is non-standard.


117-131: Clean KnownDevice creation via constructor.

The early return pattern for existing devices and the constructor-based creation for new devices aligns with the PR's migration to constructor-based initialization.


56-57: Type assertion on empty object may be unsafe.

Casting {} to ButtplugIoDeviceAttributes assumes the Record type allows missing keys. If ButtplugIoDeviceAttributes expects all keys to be present, this could cause runtime issues. Consider using Partial<ButtplugIoDeviceAttributes> or verifying the type definition allows sparse records.

#!/bin/bash
# Check ButtplugIoDeviceAttributes type definition
rg -n "ButtplugIoDeviceAttributes" --type=ts -A3 -B1 src/device/protocol/buttplugIo/
src/settings/knownDevice.ts (1)

6-57: Clean encapsulation pattern with private fields and getters.

The refactor to use private readonly fields with public getters aligns well with established patterns. The @Expose decorators correctly map internal field names to external API names for serialization.

Verify that the JsonObject type used on lines 22 and 25 is properly defined and imported. If this type is not globally declared (e.g., in a .d.ts file), explicit imports should be added to avoid potential compilation issues in strict mode.

src/device/attribute/listDeviceAttribute.ts (2)

1-17: LGTM! Clean type declarations and class structure.

The generic type constraints and class declaration are well-designed. The ListDeviceAttribute class correctly extends DeviceAttribute<V> and allows flexible key-value typing with string|Int as the base type.


19-52: LGTM! Well-structured factory methods.

The constructor and factory methods follow a consistent pattern with other attribute classes. The type narrowing in createInitialized correctly ensures the value type is IKey (non-undefined), while create allows undefined values.

src/device/protocol/buttplugIo/buttplugIoDevice.ts (4)

10-17: LGTM! Well-structured type definitions.

The template literal types for attribute keys and the Record-based attribute container are correctly implemented. This provides strong typing for buttplug.io device attributes.


20-39: LGTM! Constructor correctly updated for typed attributes.

The class declaration and constructor properly use the generic Device<ButtplugIoDeviceAttributes> pattern, maintaining type safety throughout the attribute handling.


84-93: LGTM! Clean method signature and implementation.

The send method has a clear signature with typed parameters and correctly constructs the buttplug.io command object.


41-46: Add array bounds check before accessing value[0].

Line 44 accesses value[0] without verifying the array is non-empty. The buttplug protocol documentation specifies that SensorReading.Data is an array of signed integers with varying value counts (referenced against SensorRange), but does not explicitly guarantee non-empty arrays. Add a length check to prevent passing undefined to Int.from():

public async refreshData(): Promise<void> {
    for (const sensor of this.buttplugClientDevice.messageAttributes.SensorReadCmd ?? []) {
        const value = await this.buttplugClientDevice.sensorRead(sensor.Index, sensor.SensorType);
        if (value.length > 0) {
            this.attributes[`${sensor.SensorType}-${sensor.Index}`].value = Int.from(value[0]);
        }
    }
}
src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts (3)

10-22: LGTM! Clean type definition.

The TtsVirtualDeviceAttributes type is well-structured with appropriate attribute types for each property. The class correctly implements the generic VirtualDeviceLogic interface.


32-74: LGTM! Safe attribute handling with proper undefined checks.

The refreshData method correctly uses the typed attribute model. The non-null assertions on lines 33-35 are safe because these attributes are required in TtsVirtualDeviceAttributes. The undefined handling for textToSpeak (lines 61-64) properly prevents errors.

Note: The previous review concern about setting text to null has been resolved—the code now uses undefined (line 43), which is type-safe.


80-113: LGTM! Attribute configuration is correct.

The configureAttributes method properly initializes all attributes using the appropriate factory methods. The parameter ordering (e.g., line 103 with undefined for unit-of-measure and Int.ZERO for initial value) is correct.

src/device/protocol/slvCtrlPlus/slvCtrlPlusMessageParser.ts (3)

21-54: LGTM! Clean refactor to map-based attributes.

The method correctly parses attributes into a keyed object structure. The early returns for invalid input and the validation of attribute parts (line 40) provide good error handling.

The type cast on line 23 is safe because createAttributeFromValue ensures only valid attributes are added, and line 50 uses the attribute's name as the key.


56-78: LGTM! Improved implementation with cleaner parsing.

The refactored parseStatus method uses array destructuring and modern JavaScript patterns effectively. The change from null to undefined for the return type is more idiomatic for TypeScript.


127-137: LGTM! Clean enum mapping.

The getAttributeTypeFromStr method correctly maps string values to DeviceAttributeModifier enum values with proper error handling for invalid inputs.

src/device/protocol/zc95/zc95Device.ts (8)

18-35: LGTM! Well-designed type hierarchy.

The type definitions effectively model the ZC95 device's attribute structure with required base attributes and optional dynamic attributes for power channels and patterns. The use of Partial<> and Required<> provides the right level of flexibility.


52-69: LGTM! Clean constructor and refreshData implementation.

The constructor correctly accepts and forwards the typed attributes, and refreshData is appropriately simplified to delegate to message processing.


71-95: LGTM! Well-structured setAttribute with type-safe routing.

The setAttribute method correctly routes to specialized handlers based on attribute name patterns. The type casts are safe given the generic constraints, and the error handling provides clear feedback.


97-111: LGTM! Type-safe pattern detail handling.

The method correctly uses type guards (IntRangeDeviceAttribute.isInstance and ListDeviceAttribute.isInstance) along with isValidValue checks before updating the attribute. This ensures type safety at runtime.


113-147: LGTM! Safe power channel handling with type guards.

The setAttributePowerChannel method uses the allPowerChannelValuesDefined type guard to ensure all power channel attributes have values before accessing them. This prevents runtime errors and provides proper type narrowing.


150-185: LGTM! Well-structured pattern state management.

The methods correctly manage pattern state transitions with proper checks and attribute updates. The use of Object.assign on line 175 appropriately merges dynamic attributes when a pattern starts.


187-217: LGTM! Clean attribute initialization and cleanup.

The getChannelPowerAttributes method correctly creates default power channel attributes, and removePatternAttributesAndData safely removes dynamic attributes using the typed keys utility.


219-253: LGTM! Robust message queue processing.

The processQueuedMessages method correctly processes power status messages with proper safeguards:

  • Limits processing to 10 messages per call to prevent blocking
  • Safely handles missing attributes with optional chaining
  • Updates both the current value and the max limit based on device feedback
src/device/device.ts (5)

5-14: LGTM! Sophisticated and well-designed type system.

The type definitions provide strong typing for the attribute-based device model:

  • DeviceAttributes allows optional attributes via undefined
  • AttributeValue<A> extracts the inner value type from an attribute
  • DeviceData<T> creates a mapped type that represents attribute values as data

This provides excellent type safety for device implementations.


17-44: LGTM! Clean generic class declaration.

The Device class is now properly generic with T extends DeviceAttributes, allowing subclasses to define their own strongly-typed attribute containers. The field type updates are consistent with this change.


46-61: LGTM! Constructor correctly updated for generic attributes.

The constructor properly accepts the generic attributes: T parameter and assigns it to the instance field, maintaining consistency with the new attribute model.


98-107: LGTM! Well-documented getAttribute implementation.

The getAttribute method correctly returns the attribute object (not just its value), with clear documentation explaining when undefined might be returned. The generic typing ensures type safety for the attribute key.


109-109: LGTM! Type-safe setAttribute signature.

The abstract setAttribute method signature uses sophisticated generic constraints to ensure type safety:

  • K extends keyof T ensures valid attribute keys
  • V extends AttributeValue<T[K]> extracts and constrains the value type
  • Promise<V> return type allows implementations to return the set value

This provides compile-time safety for all device implementations.

Comment thread src/device/attribute/floatDeviceAttribute.ts
Comment thread src/device/genericDeviceUpdater.ts
Comment thread src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts
Comment thread src/device/transport/serialDeviceTransport.ts
Comment thread src/logging/PinoLogger.ts Outdated
Comment thread src/logging/PinoLogger.ts Outdated
Comment thread tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts Outdated
@heavyrubberslave heavyrubberslave changed the title Unite attributes values Unite attributes values & enable strict mode Dec 26, 2025
@heavyrubberslave heavyrubberslave added the minor Creates a new minor release if merged label Dec 26, 2025
@heavyrubberslave heavyrubberslave marked this pull request as ready for review December 26, 2025 18:16

@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: 5

♻️ Duplicate comments (2)
src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts (1)

83-100: LGTM!

The guard condition on line 84 correctly returns early when the client is undefined OR not connected, ensuring discovery only proceeds when properly connected. The added guard on lines 93-95 checking isScanning before stopping is a good defensive addition.

src/logging/PinoLogger.ts (1)

12-17: Critical: Child logger must be wrapped in PinoLogger to maintain interface contract.

This issue from the previous review remains unresolved. The method returns a raw pino.Logger, but the Logger interface expects the parameter order trace(msg, context), while pino's native API uses trace(context, msg). Lines 19-41 show that PinoLogger adapts this parameter order, but child loggers returned directly from this.pino.child() will not have this adaptation, causing runtime errors when callers use the Logger interface methods.

🔎 Proposed fix
     public child(bindings?: ChildLoggerBindings, options?: ChildLoggerOptions): Logger {
-        return this.pino.child(
+        return new PinoLogger(this.pino.child(
             bindings ?? {} as pino.Bindings,
             undefined === options ? undefined : options as pino.ChildLoggerOptions
-        );
+        ));
     }

Based on past review comments.

🧹 Nitpick comments (5)
src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts (2)

58-62: Consider storing interval handles for cleanup.

The setInterval calls create timers that are never cleaned up. If this provider is expected to live for the entire application lifetime, this is acceptable. However, if there's any scenario where the provider could be destroyed or re-initialized, these intervals would leak.

🔎 Proposed fix
 private connectedDevices: Map<number, ButtplugIoDevice> = new Map();
 private deviceUpdaters: Map<number, NodeJS.Timeout> = new Map();
+private reconnectInterval?: NodeJS.Timeout;
+private scanInterval?: NodeJS.Timeout;

 // In init():
-setInterval(() => { void this.connectToServer() }, 3000);
+this.reconnectInterval = setInterval(() => { void this.connectToServer() }, 3000);

 if (this.autoScan) {
-    setInterval(() => { this.discoverButtplugIoDevices() }, 60000);
+    this.scanInterval = setInterval(() => { this.discoverButtplugIoDevices() }, 60000);
 }

Then add a destroy() method to clear these intervals when needed.


65-81: LGTM with minor suggestion.

The connection logic and error handling are correct. The guard on lines 66-69 properly skips connection when already connected or uninitialized.

Minor nit: The URL is constructed twice (line 42 and line 73). Consider storing it as an instance field or reusing this.websocketAddress directly in the log message to avoid duplication.

src/device/genericDeviceUpdater.ts (1)

29-29: Missing semicolon for consistency.

Line 29 is missing a semicolon after setAttribute, while other statements in the file use semicolons consistently.

🔎 Proposed fix
-                await device.setAttribute(attrKey, attrStr)
+                await device.setAttribute(attrKey, attrStr);
src/logging/PinoLogger.ts (1)

15-15: Simplify the redundant ternary operator.

Since options is already optional and pino's child method accepts an optional second parameter, the explicit undefined check is unnecessary.

🔎 Proposed fix
         return this.pino.child(
             bindings ?? {} as pino.Bindings,
-            undefined === options ? undefined : options as pino.ChildLoggerOptions
+            options as pino.ChildLoggerOptions | undefined
         );
src/device/attribute/floatDeviceAttribute.ts (1)

1-1: Add .js extension for ESM consistency.

Line 1 is missing the .js extension in the import path, while other imports (lines 2-3) include it. For consistency with ESM module resolution used throughout the codebase:

🔎 Proposed fix
-import {DeviceAttributeModifier, NotJustUndefined} from "./deviceAttribute";
+import {DeviceAttributeModifier, NotJustUndefined} from "./deviceAttribute.js";
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0d6fd39 and a8b536d.

📒 Files selected for processing (14)
  • src/device/attribute/floatDeviceAttribute.ts
  • src/device/attribute/intDeviceAttribute.ts
  • src/device/genericDeviceUpdater.ts
  • src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts
  • src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts
  • src/device/protocol/slvCtrlPlus/slvCtrlPlusDeviceFactory.ts
  • src/device/protocol/zc95/zc95Device.ts
  • src/device/protocol/zc95/zc95DeviceFactory.ts
  • src/device/protocol/zc95/zc95SerialDeviceProvider.ts
  • src/logging/Logger.ts
  • src/logging/PinoLogger.ts
  • tests/unit/controller/getDevicesController.spec.ts
  • tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts
  • tests/unit/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.spec.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts
  • src/device/protocol/zc95/zc95SerialDeviceProvider.ts
  • tests/unit/controller/getDevicesController.spec.ts
  • src/device/protocol/zc95/zc95DeviceFactory.ts
🧰 Additional context used
🧬 Code graph analysis (9)
tests/unit/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.spec.ts (3)
src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts (1)
  • SlvCtrlPlusDeviceAttributes (7-7)
src/device/transport/deviceTransport.ts (1)
  • DeviceTransport (1-26)
tests/unit/device/testDevice.ts (1)
  • TestDevice (3-27)
src/logging/Logger.ts (1)
src/device/transport/serialPortObserver.ts (1)
  • constructor (20-26)
src/device/attribute/intDeviceAttribute.ts (2)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/device/attribute/intGenericDeviceAttribute.ts (2)
  • IntGenericDeviceAttribute (4-12)
  • fromString (9-11)
src/logging/PinoLogger.ts (2)
src/logging/Logger.ts (3)
  • ChildLoggerBindings (1-3)
  • ChildLoggerOptions (5-7)
  • Logger (9-19)
src/serviceProvider/loggerServiceProvider.ts (1)
  • LoggerServiceProvider (7-24)
src/device/attribute/floatDeviceAttribute.ts (3)
src/device/attribute/deviceAttribute.ts (3)
  • NotJustUndefined (4-4)
  • value (53-55)
  • value (57-59)
src/util/numbers.ts (2)
  • Float (17-17)
  • Float (20-29)
src/device/attribute/numberDeviceAttribute.ts (1)
  • uom (23-25)
src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts (3)
src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts (1)
  • SlvCtrlPlusDeviceAttributes (7-7)
src/device/device.ts (2)
  • AttributeValue (8-8)
  • DeviceAttributes (6-6)
src/device/attribute/boolDeviceAttribute.ts (1)
  • BoolDeviceAttribute (6-32)
src/device/protocol/zc95/zc95Device.ts (6)
src/device/attribute/listDeviceAttribute.ts (2)
  • InitializedListDeviceAttribute (6-9)
  • ListDeviceAttribute (11-73)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/device/attribute/boolDeviceAttribute.ts (1)
  • InitializedBoolDeviceAttribute (4-4)
src/device/attribute/intRangeDeviceAttribute.ts (2)
  • IntRangeDeviceAttribute (9-81)
  • InitializedIntRangeDeviceAttribute (7-7)
src/device/device.ts (1)
  • AttributeValue (8-8)
src/util/objects.ts (1)
  • getTypedKeys (3-5)
src/device/protocol/slvCtrlPlus/slvCtrlPlusDeviceFactory.ts (2)
src/device/protocol/slvCtrlPlus/slvCtrlPlusMessageParser.ts (1)
  • SlvCtrlPlusMessageParser (13-138)
src/settings/knownDevice.ts (1)
  • serialNo (39-41)
src/device/genericDeviceUpdater.ts (2)
src/device/device.ts (1)
  • DeviceData (11-14)
src/util/objects.ts (1)
  • getTypedKeys (3-5)
🔇 Additional comments (27)
src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts (2)

16-17: LGTM!

Making these fields optional correctly reflects that they're initialized in init() rather than the constructor, improving type safety.


123-131: LGTM!

Good defensive addition. The guard prevents potential runtime errors when a device isn't found in the map, which could occur during rapid disconnect/reconnect scenarios or race conditions. The warning log aids debugging without causing a crash.

src/device/genericDeviceUpdater.ts (2)

17-35: Approve the async/await refactor with improved attribute handling.

The refactor to async/await is well-executed and improves maintainability. The use of getTypedKeys provides better type safety, and the per-attribute try-catch ensures that one failing attribute won't block updates to others. The previous concern about falsy checks has been improved by using an explicit undefined comparison instead of a negation operator.


20-20: Current implementation is correct; null check is unnecessary.

The getAttribute() method has a return type of Promise<T[K] | undefined> (see src/device/device.ts:104), meaning it returns either the attribute value or undefined — never null. The current check for undefined === await device.getAttribute(attrKey) is appropriate and correctly handles the method's actual contract.

The previous improvement from a falsy check to an explicit undefined check was the right approach, properly distinguishing between non-existent attributes and falsy-but-valid attribute values like 0, false, and empty strings.

src/logging/Logger.ts (1)

1-11: LGTM! The new child logger API resolves the parameter order mismatch.

The introduction of typed ChildLoggerBindings and ChildLoggerOptions provides better type safety than the previous (config?: object, context?: unknown) signature, and the parameter order now matches between the Logger interface and PinoLogger implementation, resolving the critical issue from the previous review. The usage pattern in src/device/transport/serialPortObserver.ts confirms the API works as intended.

Based on past review comments.

src/device/protocol/zc95/zc95Device.ts (8)

18-35: Well-structured type definitions for the new attribute model.

The type hierarchy correctly models the device's attribute structure with required base attributes and optional dynamic attributes for power channels and patterns.


48-70: Constructor properly updated for typed attributes.

The addition of @Expose decorator on fwVersion and the updated constructor signature align with the new attribute-centric model.


76-100: Type-safe setAttribute implementation.

The generic signature ensures compile-time type safety for attribute names and values, and the branching logic correctly handles all attribute types.


102-116: Proper type-guarded attribute handling.

The use of type guards ensures safe access to attribute-specific properties, and value updates occur after successful transport calls.


155-167: Correct pattern switching logic.

The method properly handles state transitions by stopping the current pattern before switching and uses Int.from for type safety.


169-190: Dynamic attribute management implemented correctly.

The method properly fetches pattern details and dynamically adds attributes using Object.assign, ensuring the attribute model reflects the active pattern's requirements.


213-222: Type-safe attribute cleanup.

The use of getTypedKeys ensures type-safe iteration while dynamically removing pattern and power channel attributes.


295-312: Well-implemented type guards.

The type guard functions correctly narrow types using runtime checks, enabling type-safe attribute and message handling.

src/device/attribute/intDeviceAttribute.ts (3)

1-7: LGTM!

The imports and type definitions are well-structured and follow the established pattern for device attributes.


8-27: LGTM!

The static factory methods provide a clean API for creating integer attributes with or without initial values, maintaining type safety throughout.


29-37: LGTM!

The fromString implementation correctly parses integers with proper validation. The use of Int.from() ensures type-safe conversion to the branded Int type, matching the pattern established in the codebase.

src/device/attribute/floatDeviceAttribute.ts (3)

5-18: LGTM!

The type definitions and constructor implementation are clean and follow the established attribute pattern.


20-37: LGTM!

The static factory methods provide a clean API consistent with other attribute classes in this refactoring.


39-47: Excellent fix - proper type-safe Float conversion implemented!

The previous review concern about bypassing Float validation has been resolved. Line 46 now correctly uses Float.from(num), which validates that the number is finite and not NaN before returning the branded Float type. This ensures type safety and data integrity.

tests/unit/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.spec.ts (2)

1-22: LGTM!

The test setup with the createDevice helper function is well-structured and provides a clean way to instantiate devices for testing with different attribute configurations.


24-36: LGTM!

The test correctly validates error handling when attempting to set a non-existent attribute. The test structure follows best practices with clear arrange/act/assert sections.

src/device/protocol/slvCtrlPlus/slvCtrlPlusDeviceFactory.ts (2)

38-61: LGTM!

The device creation logic correctly handles the new numeric fwVersion type and map-based deviceAttrs structure, aligning with the broader attribute refactoring.


63-80: LGTM!

The refactoring to use the parameterized KnownDevice constructor is cleaner and more maintainable. The change from null to undefined checks is also more idiomatic for TypeScript.

src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts (4)

1-7: LGTM!

All import statements now correctly include .js extensions, maintaining consistency with ESM module resolution throughout the codebase. The previous review concern has been addressed.


9-40: LGTM!

The constructor signature correctly reflects the new numeric fwVersion type and map-based SlvCtrlPlusDeviceAttributes structure, maintaining consistency with the broader refactoring.


42-64: LGTM!

The refreshData method now properly handles the map-based attributes with appropriate undefined checks and uses attribute-specific fromString methods for type-safe parsing. The previous concern about null assignment has been resolved.


106-118: LGTM!

The accessor methods are straightforward and correctly expose the device's attributes and configuration values.

Comment thread src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts
Comment thread src/device/protocol/zc95/zc95Device.ts
Comment thread src/device/protocol/zc95/zc95Device.ts
Comment thread src/device/protocol/zc95/zc95Device.ts Outdated
Comment thread src/device/protocol/zc95/zc95Device.ts

@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: 0

🧹 Nitpick comments (1)
src/device/transport/serialDeviceTransport.ts (1)

24-38: Previous concern addressed; implementation looks solid.

The undefined handling issue from the previous review has been resolved. The code now properly guards against undefined values by checking that all three optional fields (vendorId, productId, locationId) are defined before constructing the composite identifier. This ensures no "undefined" strings appear in device identifiers.

The three-tier fallback strategy (serialNumber → full composite → path) is sound and provides good coverage for various hardware/driver scenarios.

💡 Optional enhancement: partial composite fallback

Consider using a partial composite identifier when only some fields are available (e.g., vendorId and productId but not locationId). This could provide more meaningful identifiers than falling back to path on systems where the path might change across reboots:

 public getDeviceIdentifier(): string {
     const portInfo = this.serialPort.getPortInfo();

     if (undefined !== portInfo.serialNumber) {
         return portInfo.serialNumber;
     }

     // Fall back to some (hopefully) unique combination if serial number is missing
     if (undefined !== portInfo.vendorId && undefined !== portInfo.productId && undefined !== portInfo.locationId) {
         return `${portInfo.vendorId}-${portInfo.productId}-${portInfo.locationId}`;
     }

+    // Partial composite if we have vendorId and productId
+    if (undefined !== portInfo.vendorId && undefined !== portInfo.productId) {
+        return `${portInfo.vendorId}-${portInfo.productId}`;
+    }
+
     // If only the bare minimum is available
     return portInfo.path;
 }

Note: This is optional—the current implementation is correct and may be preferable if path stability is sufficient on your target platforms.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8b536d and 7f30e55.

📒 Files selected for processing (1)
  • src/device/transport/serialDeviceTransport.ts

@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: 2

♻️ Duplicate comments (2)
src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts (1)

96-100: Verify: Device response handling differs from past review recommendation.

The past review recommended returning the input value directly (like other device implementations), but the current code parses the device response and returns that:

const result = await this.send(`set-${attributeName.toString()} ${valueToSend.toString()}`);
attr.value = attr.fromString(result);
return attr.value as V;

If the device echoes back a different format or value, this could cause inconsistency. Other device implementations (ButtplugIoDevice, Zc95Device, VirtualDevice) set the attribute to the input value and return it directly.

Verify whether the SlvCtrl protocol's set-* response should be authoritative, or if the input value should be returned for consistency.

#!/bin/bash
# Compare setAttribute pattern across device implementations
echo "=== GenericSlvCtrlPlusDevice pattern ==="
rg -n "attr\.value.*=.*attr\.fromString" src/device/protocol/slvCtrlPlus/ --type=ts -B2 -A2

echo -e "\n=== Other device implementations ==="
rg -n "setAttribute" src/device/protocol/virtual/virtualDevice.ts src/device/protocol/zc95/zc95Device.ts src/device/protocol/buttplugIo/buttplugIoDevice.ts --type=ts -A10 | head -80
src/device/protocol/zc95/zc95Device.ts (1)

258-258: Resolve the field selection uncertainty.

The comment // or channel.OutputPower? indicates unresolved uncertainty about which field to use from the PowerStatusMessage. This should be clarified by consulting the device protocol specification or documentation, and the comment removed once the correct field is confirmed.

🧹 Nitpick comments (2)
src/device/attribute/deviceAttribute.ts (1)

62-65: Consider making getType() abstract for consistency.

Lines 67 and 69 define fromString and isValidValue as abstract methods, which enforces implementation at compile time. The getType() method serves the same purpose (all concrete attribute classes override it), but currently throws at runtime instead of being abstract.

🔎 Proposed refactor
     @Expose({ name: "type" })
-    public getType(): string {
-        throw new Error(`Not implemented`);
-    }
+    public abstract getType(): string;
src/device/attribute/intRangeDeviceAttribute.ts (1)

72-80: Use Int.from() for consistency with the branded type pattern.

Line 79 directly casts the parsed integer to T without using Int.from(), which is inconsistent with FloatDeviceAttribute (line 46) that uses Float.from(). While parseInt guarantees an integer result, using Int.from() ensures proper validation and maintains consistency with the branded type system.

🔎 Proposed fix
     public fromString(value: string): T {
         const res = parseInt(value, 10);
 
         if (isNaN(res)) {
             throw new Error(`Could not convert '${value}' to a valid value for ${this.constructor.name}`);
         }
 
-        return res as T;
+        return Int.from(res) as T;
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f30e55 and a4ba4b6.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • package.json
  • src/device/attribute/boolDeviceAttribute.ts
  • src/device/attribute/deviceAttribute.ts
  • src/device/attribute/floatDeviceAttribute.ts
  • src/device/attribute/intDeviceAttribute.ts
  • src/device/attribute/intRangeDeviceAttribute.ts
  • src/device/attribute/listDeviceAttribute.ts
  • src/device/attribute/strDeviceAttribute.ts
  • src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts
  • src/device/protocol/zc95/zc95Device.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/device/attribute/boolDeviceAttribute.ts
  • src/device/attribute/intDeviceAttribute.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-26T21:24:04.798Z
Learnt from: heavyrubberslave
Repo: SlvCtrlPlus/slvctrlplus-server PR: 54
File: src/device/protocol/zc95/zc95Device.ts:192-211
Timestamp: 2025-12-26T21:24:04.798Z
Learning: In ZC95Device power channel attributes (src/device/protocol/zc95/zc95Device.ts), the minimum value is always 0 (Int.ZERO) by design, representing zero power output. Only the maximum value is updated from PowerStatusMessage. This asymmetric range initialization is intentional and correct.

Applied to files:

  • src/device/protocol/zc95/zc95Device.ts
  • src/device/attribute/intRangeDeviceAttribute.ts
🧬 Code graph analysis (5)
src/device/attribute/floatDeviceAttribute.ts (3)
src/device/attribute/deviceAttribute.ts (3)
  • NotJustUndefined (4-4)
  • value (50-52)
  • value (54-56)
src/util/numbers.ts (2)
  • Float (17-17)
  • Float (20-29)
src/device/attribute/numberDeviceAttribute.ts (1)
  • uom (23-25)
src/device/attribute/deviceAttribute.ts (1)
src/util/numbers.ts (4)
  • Int (2-2)
  • Int (5-14)
  • Float (17-17)
  • Float (20-29)
src/device/protocol/zc95/zc95Device.ts (6)
src/device/attribute/listDeviceAttribute.ts (2)
  • InitializedListDeviceAttribute (6-9)
  • ListDeviceAttribute (11-77)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/device/attribute/boolDeviceAttribute.ts (1)
  • InitializedBoolDeviceAttribute (4-4)
src/device/attribute/intRangeDeviceAttribute.ts (2)
  • IntRangeDeviceAttribute (9-85)
  • InitializedIntRangeDeviceAttribute (7-7)
src/device/device.ts (1)
  • AttributeValue (8-8)
src/util/objects.ts (1)
  • getTypedKeys (3-5)
src/device/attribute/strDeviceAttribute.ts (1)
src/device/attribute/deviceAttribute.ts (7)
  • NotJustUndefined (4-4)
  • name (35-37)
  • label (39-41)
  • modifier (43-45)
  • value (50-52)
  • value (54-56)
  • NotUndefined (5-5)
src/device/attribute/intRangeDeviceAttribute.ts (3)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/device/attribute/intDeviceAttribute.ts (1)
  • IntAttributeValue (5-5)
src/device/attribute/deviceAttribute.ts (5)
  • name (35-37)
  • label (39-41)
  • modifier (43-45)
  • value (50-52)
  • value (54-56)
🔇 Additional comments (25)
src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts (7)

1-7: Imports look good.

All imports now include the .js extension for ESM compatibility, addressing the previous review comment.


19-19: LGTM: fwVersion type change.

The type change from string to number is consistent across the property declaration, constructor parameter, and assignment.

Also applies to: 25-25, 38-38


70-82: LGTM: Comprehensive validation logic.

The attribute existence checks, null/undefined guards, and value validation provide good defensive programming with clear error messages.


87-94: LGTM: Boolean conversion for protocol.

The boolean-to-numeric conversion (1=true, 0=false) is correctly implemented with appropriate type checking and a clear comment explaining the protocol requirement.


106-109: LGTM: Method renamed consistently.

The getAttributes() method properly exposes the device attributes, aligning with the broader refactoring from array-based to keyed attribute structure.


111-117: LGTM: Timeout and interval getters.

The serial timeout and refresh interval methods are straightforward and correctly implemented.


62-62: No action needed on line 62.

AttributeValue is defined as NotJustUndefined<string | Int | Float | boolean | null | undefined> in src/device/attribute/deviceAttribute.ts, which explicitly includes undefined. Assigning undefined when dataObj[attrKey] is an empty string is type-safe and valid.

Likely an incorrect or invalid review comment.

src/device/attribute/strDeviceAttribute.ts (1)

1-36: LGTM! Clean implementation of string attribute type.

The StrDeviceAttribute class follows the established attribute pattern with appropriate factory methods, type checking, and string handling. The implementation is straightforward and type-safe.

src/device/attribute/deviceAttribute.ts (1)

1-74: Strong foundation for the attribute system.

The base DeviceAttribute class provides a well-designed abstraction with:

  • Clear type utilities (NotJustUndefined, NotUndefined)
  • Appropriate serialization decorators
  • Proper encapsulation with getters/setters
  • Abstract contract for concrete implementations
src/device/attribute/floatDeviceAttribute.ts (1)

39-47: LGTM! Proper use of Float.from() for type-safe conversion.

The fromString method correctly validates the parsed number and uses Float.from(num) to ensure the value is finite and properly branded as a Float type. This addresses the previous review concern and aligns with the branded type pattern.

src/device/attribute/intRangeDeviceAttribute.ts (1)

1-85: Well-designed range attribute implementation.

The IntRangeDeviceAttribute class provides comprehensive range management with:

  • Proper serialization decorators for min/max/incrementStep
  • Type-safe factory methods
  • Clear getters/setters for range bounds
src/device/attribute/listDeviceAttribute.ts (1)

1-77: LGTM! Flexible and type-safe list attribute implementation.

The ListDeviceAttribute class provides:

  • Flexible support for both string and Int keys/values
  • Proper validation via isValidValue against the permitted values map
  • Type-safe factory methods with generics
  • Clean serialization support

The fromString implementation appropriately handles both integer and string inputs, with runtime validation in isValidValue ensuring only permitted values are accepted.

src/device/protocol/zc95/zc95Device.ts (13)

1-17: LGTM - Imports support attribute-centric refactor.

The new imports properly support the refactoring from data-centric to attribute-centric model. The addition of typeDetect for error messages (used on line 121) and getTypedKeys for type-safe iteration (used on line 222) are good improvements.


19-36: LGTM - Well-structured attribute types.

The type hierarchy properly models the device state with required base attributes and optional dynamic attributes (power channels and pattern attributes). The use of Partial and Required correctly expresses the attribute lifecycle.


56-71: LGTM - Constructor properly updated.

The constructor signature correctly reflects the new attribute-centric model. The addition of fwVersion as a serializable field with @Expose() is appropriate.


73-77: LGTM - Correctly handles synchronous processing.

The method appropriately calls the synchronous processQueuedMessages() without await and returns Promise.resolve() to satisfy the Promise<void> return type required by the parent class.


79-106: LGTM - Type-safe attribute setter.

The generic signature properly constrains the value type based on the attribute key, ensuring type safety. The routing logic covers all attribute types with a clear error for unhandled cases.


108-124: LGTM - Robust pattern detail handling.

The method properly validates attribute types and values before updating transport, with excellent error messages using typeDetect. The flow correctly updates the attribute value after successful transport communication.


153-160: LGTM - Proper type guard implementation.

The type predicate correctly narrows the partial power channel attributes to fully initialized attributes, ensuring type safety for operations that require all channels to be defined.


163-175: LGTM - Correct pattern switching logic.

The method properly handles the pattern lifecycle by ensuring the current pattern is stopped before switching to a new one. The early return optimization avoids unnecessary operations.


177-198: LGTM - Comprehensive pattern lifecycle management.

The method correctly handles both pattern start and stop operations, including dynamic attribute creation/removal and proper transport communication. The use of Object.assign to merge attributes is appropriate.


221-230: LGTM - Type-safe attribute cleanup.

The use of getTypedKeys for type-safe iteration over attribute keys is excellent. The cleanup logic correctly removes both pattern and power channel attributes.


232-262: Good refactor - Promise anti-pattern resolved.

The method has been correctly refactored from the Promise constructor anti-pattern to a synchronous void return. However, the comment on line 258 still indicates uncertainty about field selection.


264-295: LGTM - Clean attribute creation from pattern details.

The method properly creates typed attributes using createInitialized, which sets initial values during construction. The previous issue with redundant value assignment has been resolved. Nice attention to detail with the unit of measure conversion on line 277 ('us''µs').


297-313: LGTM - Standard type guards.

The type predicates follow standard TypeScript patterns and provide proper type narrowing for the various discriminated unions and attribute types.

Comment thread package.json Outdated
Comment thread package.json Outdated

@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

Caution

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

⚠️ Outside diff range comments (1)
src/device/provider/deviceProviderFactory.ts (1)

4-7: The interface change is incomplete—three implementations do not match the updated signature.

The interface now requires create(config: JsonObject): DeviceProvider, but:

  • SlvCtrlPlusSerialDeviceProviderFactory.create() takes no parameter
  • Zc95SerialDeviceProviderFactory.create() takes no parameter
  • VirtualDeviceProviderFactory.create() takes no parameter

Only ButtplugIoWebsocketDeviceProviderFactory has the correct signature. Update the three mismatched implementations to accept the JsonObject config parameter to match the interface and match the call site in deviceProviderLoader.ts (line 48).

♻️ Duplicate comments (1)
package.json (1)

29-29: Critical: vm2 remains deprecated despite version bump.

Updating vm2 from ^3.9.14 to ^3.10.0 does not resolve the fundamental issue that the package has been deprecated by its maintainers due to multiple critical sandbox-escape vulnerabilities. Even if version 3.10.0 exists and patches some CVEs, the package is no longer maintained.

The maintainers explicitly recommend migrating to isolated-vm or other alternatives. Continuing to use vm2 poses significant security risks.

Verify the current status of vm2 3.10.0 and confirm the deprecation:

#!/bin/bash
# Check vm2 npm registry data including deprecation status
echo "=== vm2 package status ==="
npm view vm2@3.10.0 version deprecated 2>/dev/null || echo "Version 3.10.0 not found"

echo -e "\n=== Latest vm2 version and deprecation status ==="
npm view vm2 dist-tags.latest deprecated

echo -e "\n=== Recent vm2 versions ==="
npm view vm2 versions --json | jq '.[-10:]'

# Check for security advisories
echo -e "\n=== Security advisories for vm2 ==="
gh api graphql -f query='
{
  securityVulnerabilities(first: 10, ecosystem: NPM, package: "vm2") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
      firstPatchedVersion {
        identifier
      }
    }
  }
}'
Is vm2 npm package still deprecated in 2025? What is the latest version?
🧹 Nitpick comments (4)
src/schemaValidation/JsonSchemaValidator.ts (1)

24-26: Consider applying consistent null-safety pattern.

Line 21 uses the nullish coalescing operator for safety, but line 25 passes this.schemaValidator.errors directly to errorsText(). While AJV likely handles undefined gracefully, applying the same pattern would be more consistent.

🔎 Suggested consistency improvement
 public getValidationErrorsAsText(): string {
-    return this.ajv.errorsText(this.schemaValidator.errors);
+    return this.ajv.errorsText(this.schemaValidator.errors ?? []);
 }
src/device/protocol/zc95/zc95Device.ts (1)

233-263: Method now correctly returns void, addressing previous review feedback.

The Promise constructor anti-pattern has been resolved—the method now returns void and executes synchronously. The logic correctly processes queued power status messages and updates attribute values and limits.

Optional clarification: Line 259's comment // or channel.OutputPower? indicates uncertainty about the power field selection. If this is resolved, consider removing the comment or documenting why MaxOutputPower is the correct choice.

🔎 Optional: Clarify power field selection

If MaxOutputPower is confirmed as the correct field:

-                channelAttr.value = Int.from(Math.floor(channel.MaxOutputPower * 0.1)) // or channel.OutputPower?
+                channelAttr.value = Int.from(Math.floor(channel.MaxOutputPower * 0.1))

Or document why this field is used if there's a specific reason.

src/device/protocol/virtual/virtualDevice.ts (1)

48-67: Consider refactoring to use async/await instead of the Promise constructor.

The setAttribute method wraps synchronous operations in new Promise, which is an anti-pattern when no actual asynchronous work is performed. This can be simplified using async/await syntax for better readability and maintainability.

🔎 Proposed refactor using async/await
-public async setAttribute<K extends keyof T, V extends AttributeValue<T[K]>>(attributeName: K, value: V): Promise<V> {
-    return new Promise<V>((resolve, reject) => {
-        this.state = DeviceState.busy;
-
-        const attribute = this.attributes[attributeName];
-
-        if (undefined === attribute) {
-            reject(new Error(
-                `Attribute named "${attributeName.toString()}" does not exist for device with id "${this.deviceId}"`
-            ));
-            return;
-        }
-
-        attribute.value = value;
-
-        this.state = DeviceState.ready;
-
-        resolve(value);
-    });
-}
+public async setAttribute<K extends keyof T, V extends AttributeValue<T[K]>>(attributeName: K, value: V): Promise<V> {
+    this.state = DeviceState.busy;
+
+    const attribute = this.attributes[attributeName];
+
+    if (undefined === attribute) {
+        throw new Error(
+            `Attribute named "${attributeName.toString()}" does not exist for device with id "${this.deviceId}"`
+        );
+    }
+
+    attribute.value = value;
+
+    this.state = DeviceState.ready;
+
+    return value;
+}
src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts (1)

38-38: Consider simplifying the null check.

The condition checks for both undefined and null, but based on the StrDeviceAttribute type, the value should be string | undefined, making the null check unnecessary. This might be legacy code from the previous implementation.

🔎 Proposed simplification
-        if (undefined !== text && null !== text) {
+        if (undefined !== text) {
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4ba4b6 and 4194819.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (13)
  • package.json
  • src/controller/settings/putSettingsController.ts
  • src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts
  • src/device/protocol/virtual/randomGenerator/randomGeneratorVirtualDeviceLogic.ts
  • src/device/protocol/virtual/virtualDevice.ts
  • src/device/protocol/zc95/zc95Device.ts
  • src/device/provider/deviceProviderFactory.ts
  • src/schemaValidation/JsonSchemaValidator.ts
  • src/schemaValidation/JsonSchemaValidatorFactory.ts
  • src/settings/deviceSource.ts
  • src/settings/knownDevice.ts
  • src/settings/settingsManager.ts
  • src/types.d.ts
✅ Files skipped from review due to trivial changes (1)
  • src/controller/settings/putSettingsController.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/settings/deviceSource.ts
  • src/device/protocol/virtual/randomGenerator/randomGeneratorVirtualDeviceLogic.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-26T21:24:04.798Z
Learnt from: heavyrubberslave
Repo: SlvCtrlPlus/slvctrlplus-server PR: 54
File: src/device/protocol/zc95/zc95Device.ts:192-211
Timestamp: 2025-12-26T21:24:04.798Z
Learning: In ZC95Device power channel attributes (src/device/protocol/zc95/zc95Device.ts), the minimum value is always 0 (Int.ZERO) by design, representing zero power output. Only the maximum value is updated from PowerStatusMessage. This asymmetric range initialization is intentional and correct.

Applied to files:

  • src/device/protocol/zc95/zc95Device.ts
🧬 Code graph analysis (4)
src/schemaValidation/JsonSchemaValidator.ts (2)
src/serviceProvider/schemaValidationServiceProvider.ts (2)
  • SchemaValidationServiceProvider (7-23)
  • Ajv (10-15)
src/serviceProvider/settingsServiceProvider.ts (1)
  • jsonSchemaValidatorFactory (13-20)
src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts (5)
src/device/attribute/strDeviceAttribute.ts (1)
  • StrDeviceAttribute (6-36)
src/device/attribute/boolDeviceAttribute.ts (1)
  • BoolDeviceAttribute (6-36)
src/device/attribute/intDeviceAttribute.ts (1)
  • IntDeviceAttribute (8-42)
src/device/protocol/virtual/virtualDeviceLogic.ts (1)
  • VirtualDeviceLogic (4-11)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
src/device/protocol/virtual/virtualDevice.ts (3)
src/device/device.ts (2)
  • DeviceAttributes (6-6)
  • AttributeValue (8-8)
src/device/protocol/virtual/virtualDeviceLogic.ts (1)
  • VirtualDeviceLogic (4-11)
src/device/attribute/deviceAttribute.ts (3)
  • AttributeValue (6-6)
  • value (50-52)
  • value (54-56)
src/device/protocol/zc95/zc95Device.ts (7)
src/device/attribute/listDeviceAttribute.ts (2)
  • InitializedListDeviceAttribute (6-9)
  • ListDeviceAttribute (11-77)
src/device/attribute/boolDeviceAttribute.ts (1)
  • InitializedBoolDeviceAttribute (4-4)
src/device/attribute/intRangeDeviceAttribute.ts (2)
  • IntRangeDeviceAttribute (9-85)
  • InitializedIntRangeDeviceAttribute (7-7)
src/types.d.ts (1)
  • AllOrNone (11-11)
src/device/protocol/zc95/Zc95Messages.ts (2)
  • Zc95Messages (75-282)
  • MsgResponse (4-10)
src/device/device.ts (1)
  • AttributeValue (8-8)
src/util/objects.ts (1)
  • getTypedKeys (3-5)
🔇 Additional comments (21)
src/types.d.ts (1)

11-11: LGTM! Useful utility type.

The AllOrNone<T> type correctly implements an "all or nothing" constraint pattern, allowing either a complete T or an empty object.

src/schemaValidation/JsonSchemaValidatorFactory.ts (1)

4-4: Good addition for type safety.

The JsonObject import and usage at line 16 appropriately adds type information to the parsed schema data without changing runtime behavior.

Also applies to: 16-16

src/schemaValidation/JsonSchemaValidator.ts (2)

3-3: LGTM!

The JsonObject import adds appropriate typing for the validate method parameter.


21-21: Good defensive programming.

Using nullish coalescing ensures getValidationErrors() always returns an array, even when this.schemaValidator.errors is undefined.

src/settings/settingsManager.ts (3)

12-12: LGTM!

JsonObject import supports type-safe handling of parsed settings data.


119-123: LGTM! Simplified construction.

The inline DeviceSource instantiation is more concise and maintains the same functionality.


23-23: The undefined handling is properly implemented across all callers.

The refactor from null to undefined semantics is correctly implemented. getSettings() returns Settings|undefined as expected, and the one identified caller in src/device/protocol/virtual/virtualDeviceProvider.ts (line 44) properly checks for undefined before using the settings object. The internal save() method also correctly guards against undefined values.

src/settings/knownDevice.ts (2)

36-58: LGTM! Clean getter implementation.

The public getters provide read-only access to all encapsulated fields, maintaining the expected API surface while enforcing immutability.


7-34: Good encapsulation with private readonly fields and explicit getters.

The refactor to private readonly fields with explicit getters provides proper encapsulation and makes KnownDevice immutable after construction. The constructor enables clean initialization and the @Expose decorators maintain serialization compatibility. Verification confirms no code attempts to set properties after construction, so there are no breaking mutations.

src/device/protocol/zc95/zc95Device.ts (10)

1-37: LGTM! Well-structured attribute type definitions.

The imports and type definitions correctly model the attribute-centric refactoring. The use of AllOrNone for power channel attributes ensures they're either fully present or completely omitted, which aligns with the deferred initialization pattern used elsewhere in the code.


57-78: LGTM! Constructor and refreshData correctly structured.

The constructor properly initializes the attribute-centric model, and refreshData correctly calls processQueuedMessages() synchronously (since it now returns void rather than Promise<void>), then returns Promise.resolve() for interface consistency.


80-107: LGTM! Type-safe attribute setter with proper routing.

The generic signature with AttributeValue<Zc95DeviceAttributes[K]> ensures type safety across different attribute types. The routing logic correctly handles all attribute categories with appropriate type guards.


109-125: LGTM! Properly handles pattern detail attribute updates.

The method correctly distinguishes between IntRangeDeviceAttribute and ListDeviceAttribute types, validates values, and updates the transport before modifying the attribute state.


127-161: LGTM! Power channel updates correctly guard against partial initialization.

The silent return when power channels aren't fully initialized (lines 131-133) is intentional per previous discussion. The type guard allPowerChannelValuesDefined properly narrows the type, and the temporary data object ensures atomic transport updates.

Based on learnings, the minimum value for power channels is always 0 by design.


164-199: LGTM! Pattern lifecycle methods properly orchestrated.

The methods correctly handle state transitions: setAttributeActivePattern stops any running pattern before switching, and setAttributePatternStarted synchronizes attribute state with transport operations. The use of Object.assign to merge dynamically created attributes is appropriate for this use case.


201-220: LGTM! Power channel attribute initialization follows deferred pattern.

The method creates uninitialized attributes (with undefined values) that are populated later via processQueuedMessages(). The asymmetric range initialization (min always Int.ZERO, max updated from messages) is intentional and correct per the design.

Based on learnings, the minimum power value is always 0 by design, representing zero power output.


222-231: LGTM! Type-safe attribute cleanup.

The use of getTypedKeys ensures type-safe iteration when removing dynamically created attributes. The logic correctly identifies and removes both pattern detail and power channel attributes.


265-296: LGTM! Pattern attributes correctly initialized from device menu items.

The method properly creates initialized attributes for both range (min/max) and list (multi-choice) menu items. The redundant value assignment mentioned in previous review has been removed—values are now set only during createInitialized calls.

The unit of measure conversion ('us' → 'µs') is a nice touch for proper display.


298-314: LGTM! Type guards provide proper type narrowing.

The type predicate methods are correctly implemented and enable type-safe handling of different attribute types, message types, and menu item types throughout the class.

src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts (2)

67-74: Verify callback synchronization with async flow.

The say.speak callback updates the speaking attribute using a fire-and-forget pattern (void device.setAttribute(...)). This means the attribute update happens asynchronously after the refreshData method completes, which could lead to race conditions if refreshData is called again before the callback executes. Verify this is the intended behavior and won't cause state inconsistencies.


81-114: LGTM! Clean refactor to object-based attributes.

The refactoring from array-based to object-based attributes with typed properties is well-implemented. The use of static factory methods and proper initialization provides good type safety and clarity.

Comment thread src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts Outdated

@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

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4194819 and 69e86fe.

📒 Files selected for processing (1)
  • src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts (5)
src/device/attribute/strDeviceAttribute.ts (1)
  • StrDeviceAttribute (6-36)
src/device/attribute/boolDeviceAttribute.ts (1)
  • BoolDeviceAttribute (6-36)
src/device/attribute/intDeviceAttribute.ts (1)
  • IntDeviceAttribute (8-42)
src/device/protocol/virtual/virtualDeviceLogic.ts (1)
  • VirtualDeviceLogic (4-11)
src/util/numbers.ts (2)
  • Int (2-2)
  • Int (5-14)
🔇 Additional comments (6)
src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts (6)

1-10: LGTM! Imports aligned with typed attribute system.

The imports properly support the refactoring to strongly-typed device attributes, including the branded Int type and specific attribute classes.


11-16: LGTM! Type definition provides strong typing.

The TtsVirtualDeviceAttributes type properly constrains the attribute structure and enables type-safe access throughout the class.


34-36: LGTM! Previous unsafe attribute access resolved.

The code now correctly uses optional chaining (?.value) instead of non-null assertions, with appropriate default values for boolean attributes. This addresses the concern raised in previous reviews.


38-45: LGTM! Text clearing properly uses undefined.

Setting the text attribute to undefined (line 44) correctly clears the write-only attribute after processing. This resolves the previous review concern about incompatible null assignment.


60-65: Good defensive programming.

The check for undefined after shift() is good practice, even though the queue-empty check at lines 47-49 should typically prevent this case.


81-114: LGTM! Attribute configuration is well-structured.

The method properly:

  • Assigns appropriate modifiers (writeOnly for text, readOnly for speaking/queueLength, readWrite for queuing)
  • Initializes attributes with sensible defaults (false for booleans, Int.ZERO for queue length)
  • Returns a correctly typed object matching TtsVirtualDeviceAttributes

Comment thread src/device/protocol/virtual/audio/ttsVirtualDeviceLogic.ts Outdated
@heavyrubberslave heavyrubberslave merged commit b34c74e into main Dec 29, 2025
2 checks passed
@heavyrubberslave heavyrubberslave deleted the feat/unite-attributes-values branch December 29, 2025 21:38
heavyrubberslave added a commit to SlvCtrlPlus/slvctrlplus-frontend that referenced this pull request Dec 29, 2025
Belongs to: SlvCtrlPlus/slvctrlplus-server#54

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Added speed control UI for Striker Mk2 devices (debounced slider).
  * Added display for virtual random generator values.

* **Bug Fixes**
* Safer handling of missing/invalid device properties to avoid
NaN/undefined issues.
  * Distance readings show a placeholder when unavailable.
* Automation form now emits cancel; log viewer shows “currently not
running” and emits close reliably.

* **Improvements**
  * Dependency updates (Vuetify, Vite).
  * Charts handle gaps and tooltips more robustly.
  * Consolidated device attribute handling for more consistent UI.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This was referenced Dec 30, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Jan 6, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Mar 13, 2026
This was referenced Jun 7, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Jun 19, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Jul 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor Creates a new minor release if merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unite device attributes and corresponding data

1 participant