Skip to content

Initial air purifier support #1012

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open

Conversation

Avamander
Copy link

@Avamander Avamander commented Feb 19, 2025

I tried to follow the lead of other ServiceCreators in this project in order to try and add initial air purifier support for the IKEA Starkvind.

Unfortunately I couldn't find build instructions that would let me actually properly run the new code alongside HomeBridge. This means this PR is currently totally untested. Feedback is still welcome though.

Summary by CodeRabbit

  • New Features

    • Introduced air purifier management, allowing users to control and monitor air purifier operation within their home automation system.
  • Bug Fixes

    • Improved validation logic for air quality conversion to prevent negative sensor values from being processed.

@Avamander Avamander marked this pull request as draft February 19, 2025 23:16
Copy link

coderabbitai bot commented Feb 19, 2025

Walkthrough

This change introduces air purifier support to the home automation system. A new file defines classes and interfaces for managing air purifier service creation and state management. The functionality includes filtering exposes data to create service handlers and updating states with defined properties. Additionally, the new air purifier creator is integrated into the service creator manager for proper instantiation and error handling within the system.

Changes

File(s) Change Summary
src/converters/air_purifier.ts Added new classes and interfaces for air purifier support: AirPurifierCreator, AirPurifierHandler, AirPurifierProperty interface, WithExposesValidator, and various property classes (CurrentAirPurifierStateProperty, TargetAirPurifierStateProperty, RotationSpeedProperty, LockPhysicalControlsProperty).
src/converters/creators.ts Imported AirPurifierCreator and added it to the BasicServiceCreatorManager static constructors array, enabling the creation of air purifier services.
src/converters/air_quality.ts Modified the convertToAirQuality method in three classes to enhance validation logic for sensorValue, ensuring negative values are not processed.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Manager as BasicServiceCreatorManager
    participant Creator as AirPurifierCreator
    participant Handler as AirPurifierHandler
    participant Logger as Logger

    Client->>Manager: Provide exposes entries
    Manager->>Creator: Iterate over exposes entries
    Creator->>Handler: Create/init air purifier service
    note right of Handler: Initialize properties\n(Current, Target, Rotation Speed, Lock Controls)
    Creator-->>Logger: Log warnings/errors if setup fails
Loading
sequenceDiagram
    participant Data as Data Provider
    participant Handler as AirPurifierHandler
    participant Property as AirPurifierProperty

    Data->>Handler: Send sensor update
    Handler->>Property: Update state for each property
    Property-->>Handler: Return updated state
Loading

Poem

I'm a hopping rabbit in a code field so green,
New air purifier magic is bright and keen.
Properties dance in a synchronized beat,
While services awaken with a steady heartbeat.
Cheers from my burrow for this code delight! 🐰✨


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@itavero
Copy link
Owner

itavero commented Feb 20, 2025

Unfortunately I couldn't find build instructions that would let me actually properly run the new code alongside HomeBridge.

Please check the package.json. You should be able to run npm start to build + link the plugin and start Homebridge in your machine.

You can use npm run test to run the unit tests. There's some more scripts in there as well.

I was thinking about creating a dev container for this repository, but never got around to doing so.

@Avamander
Copy link
Author

Please check the package.json. You should be able to run npm start to build + link the plugin and start Homebridge in your machine.

You can use npm run test to run the unit tests. There's some more scripts in there as well.

I'll try that.

I was thinking about creating a dev container for this repository, but never got around to doing so.

That would be incredibly helpful.

@Avamander Avamander marked this pull request as ready for review February 27, 2025 01:03
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5b7090 and 52ed269.

📒 Files selected for processing (2)
  • src/converters/air_purifier.ts (1 hunks)
  • src/converters/creators.ts (2 hunks)
🔇 Additional comments (4)
src/converters/creators.ts (1)

13-13: Ensure test coverage for the new AirPurifierCreator.

The import statement and array inclusion look correct. However, please verify that the newly introduced AirPurifierCreator is fully tested to confirm that it works as expected in conjunction with other service creators.

Also applies to: 35-35

src/converters/air_purifier.ts (3)

1-13: Overall implementation looks solid.

• The import statements and basic class/type definitions (lines 1–13, 14–31, 33–42, 45–48, 49–53, and 55–83) align well with Homebridge patterns and existing code conventions.
• Construction and registration of the AirPurifierCreator within the accessory appear consistent with the system's plugin architecture.

Also applies to: 14-31, 33-42, 45-48, 49-53, 55-83


158-159: Verify the speed scaling factor.

Multiplying by 11.11 and dividing by 11.11 might cause off-by-one or rounding issues. Confirm that a 0–9 fan speed range is intended, and consider using a variable or fraction (e.g., 100 / 9) for clearer maintenance.

Also applies to: 163-165


201-263: Suggest additional tests for AirPurifierHandler.

The handler logic looks well-structured, but it's untested. Consider unit tests to confirm each property type is detected correctly, especially for string vs. numeric exposures.

@Avamander
Copy link
Author

I've made the build and all the tests run properly.

But practically testing it out is still difficult, I don't have a working development environment with all three pieces available. Is there a way to install a development branch/build next to existing HomeBridge installation properly?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/converters/air_purifier.ts (1)

55-83: Solid abstract base class for air purifier properties.
The PassthroughAirPurifierProperty sets up a consistent pattern for your derived classes. Using copyExposesRangeToCharacteristic ensures that the characteristic’s valid range aligns with Zigbee2MQTT definitions.

Consider clarifying the property type of this.state by introducing an enum if the possible states are known enumerations rather than free-form numeric values.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between 52ed269 and 9b00bff.

📒 Files selected for processing (1)
  • src/converters/air_purifier.ts (1 hunks)
🔇 Additional comments (8)
src/converters/air_purifier.ts (8)

1-13: Imports look good and follow established patterns.
No issues appear with these imports; the references to ./interfaces, ../z2mModels, ../hap, ../helpers, and homebridge appear consistent with the rest of the codebase.


14-31: Good use of grouped endpoints to create Air Purifier services.
The createServicesFromExposes method effectively organizes the relevant exposes by endpoint. This grouping ensures modular handling of each endpoint and avoids redundant service creation.


33-43: Proper error handling for service creation.
Catching errors in createService and logging a warning is a good practice to prevent entire service registration from failing silently.


45-48: Clear extension for canUseExposesEntry.
Defining WithExposesValidator clarifies the contract that implementers must define a canUseExposesEntry method. This is a succinct approach for your property factories.


49-53: Interface design is straightforward.
Declaring AirPurifierProperty with updateState ensures each property class has a uniform interface to update state based on raw data.


85-113: Consistent string-to-enumeration mapping for fan_state.
The logic correctly maps 'ON' to PURIFYING_AIR and 'OFF' to IDLE. Fallback to INACTIVE is sensible. The handleSetOn method properly queues 'fan_state': 'ON'/'OFF'.


174-199: child_lock logic is consistent.
Mapping LOCK and UNLOCK to a HomeKit characteristic for physical controls is fine. The fallback to CONTROL_LOCK_DISABLED if not defined is a practical approach.


201-263: Well-structured AirPurifierHandler.
The property factory approach centralizes all possible property classes in propertyFactories, making it easy to add or remove features as needed. The error thrown when no suitable exposes are found is helpful for debugging. Overall, this design fits smoothly with the existing framework.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
src/converters/air_purifier.ts (4)

14-31: Perform additional validation of exposed entries.
This logic correctly filters by AirPurifierHandler.propertyFactories; however, consider logging additional details if no valid exposes are found for debugging.


33-43: Optionally log the error stack in createService.
The try/catch appropriately warns, but logging error.stack can help with debugging.

- accessory.log.warn(
-   'Failed to setup Air Purifier service ' + `for accessory ${accessory.displayName} for endpoint ${endpoint}: ${error}`
- );
+ accessory.log.warn(
+   `Failed to setup Air Purifier service for accessory ${accessory.displayName} on endpoint ${endpoint}: `,
+   error
+ );

49-53: Consider renaming state to avoid confusion.
The property state is numeric, which may conflict with other states in the code. A more explicit name could reduce confusion.


174-199: Clarify logic for lock states.
Currently, any non-null value for sensorValue is mapped to CONTROL_LOCK_ENABLED. This might conflict if future device states are introduced. A direct check for 'LOCK' or 'UNLOCK' might be safer.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between 3ab23a0 and e981b55.

📒 Files selected for processing (1)
  • src/converters/air_purifier.ts (1 hunks)
🔇 Additional comments (4)
src/converters/air_purifier.ts (4)

1-13: All imports and utilities look good.
No immediate issues spotted. The code references the necessary modules, and the helpers appear relevant.


45-47: Type declaration is straightforward.
No issues found.


55-83: Abstract base class shows good structure.
No critical issues identified. The approach for on('set', this.handleSetOn.bind(this)) is acceptable given the design, though typically "current" states are read-only.


201-262: Good integration flow in AirPurifierHandler.
Keep an eye on potential mismatch between read-only vs. settable characteristics in HomeKit. Overall, the approach is sound.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/converters/air_purifier.ts (2)

136-136: Consider using numeric values instead of boolean casting

The HomeKit TargetAirPurifierState characteristic uses numeric values (0 for MANUAL, 1 for AUTO). The current implementation casts the value to boolean which could lead to unexpected behavior.

- data['fan_mode'] = (value as boolean) ? 'auto' : 'off';
+ data['fan_mode'] = (value as number) === 1 ? 'auto' : 'manual';

164-168: Consider renaming the device property to avoid conflict

To avoid conflict with the TargetAirPurifierState property, consider using a different property name for speed settings rather than both modifying 'fan_mode'. This depends on how the device API is designed.

If the device supports it, consider using a dedicated property for speed:

- if (speed > 0) {
-   data['fan_mode'] = speed;
- } else {
-   data['fan_mode'] = 'off';
- }
+ if (speed > 0) {
+   data['fan_speed'] = speed;
+   data['fan_mode'] = 'manual';  // Set to manual mode when speed is adjusted
+ } else {
+   data['fan_speed'] = 0;
+   data['fan_mode'] = 'off';
+ }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between e981b55 and 101798b.

📒 Files selected for processing (1)
  • src/converters/air_purifier.ts (1 hunks)
🔇 Additional comments (7)
src/converters/air_purifier.ts (7)

85-113: Review Current Air Purifier State property implementation

The HomeKit CurrentAirPurifierState characteristic is typically designed to be read-only as it reflects the actual state of the device rather than a target state. Providing a setter method in handleSetOn might be unintuitive and could conflict with HomeKit's design patterns.


115-140: Inconsistent mapping between HomeKit and device states

There's a misalignment between convertToAirPurifier() and handleSetOn(). The converter maps anything that's not "auto" to MANUAL (value 0), while handleSetOn maps falsy values to "off". This means there's no clear path to a "manual" mode with a specific speed setting when toggling through HomeKit.


142-172: Potential state conflict with RotationSpeed and TargetAirPurifierState

Both RotationSpeedProperty.handleSetOn() and TargetAirPurifierStateProperty.handleSetOn() write to the same device property 'fan_mode', but with different value types:

  • RotationSpeed sets it to numeric values or 'off'
  • TargetAirPurifierState sets it to 'auto' or 'off'

This can lead to inconsistent states depending on which control the user manipulates last.


1-43: LGTM: AirPurifierCreator implementation follows service pattern

The creator class properly filters exposes entries, groups them by endpoint, and registers service handlers. The error handling in createService is well-implemented.


55-83: Base implementation for property classes looks good

The PassthroughAirPurifierProperty abstract class provides a solid foundation for the specific property implementations. The updateState method correctly handles state updates and propagates them to the HomeKit characteristics.


233-235: Good error handling for empty properties

The check for empty properties array with a meaningful error message is a good practice that helps with debugging.


201-262:

✅ Verification successful

Code structure follows service handler pattern well

The AirPurifierHandler implementation follows the established pattern for service handlers in the project. It properly initializes properties based on exposes entries and provides appropriate error handling when no suitable entries are found.


🏁 Script executed:

#!/bin/bash
# Verify the integration of AirPurifierCreator in the service creator manager
echo "Checking for integration in creators.ts..."
grep -A 5 "AirPurifierCreator" src/converters/creators.ts || echo "AirPurifierCreator might not be integrated in creators.ts"

# Check for tests
echo "Checking for tests..."
find test -name "*air*purifier*" -o -name "*AirPurifier*"

Length of output: 573


AirPurifierHandler Implementation Verified and Approved

The code follows the established service handler pattern. Integration of the AirPurifierCreator in the ServiceCreatorManager (as seen in src/converters/creators.ts) is confirmed, and the error handling for missing exposes entries is appropriately implemented. No structural changes are needed. One minor note: while the integration check is positive, please ensure that dedicated tests for the air purifier functionality are added if they are not already part of your test suite.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/converters/air_purifier.ts (2)

127-132: ⚠️ Potential issue

Incorrect value type handling in TargetAirPurifierState setter.

HomeKit's TargetAirPurifierState uses numeric constants (0 for MANUAL, 1 for AUTO), but you're treating the incoming value as boolean. This can lead to unexpected behavior.

 handleSetOn(value: CharacteristicValue, callback: CharacteristicSetCallback): void {
   const data = {};
-  data['fan_mode'] = (value as boolean) ? 'auto' : 'off';
+  data['fan_mode'] = (value as number) === hap.Characteristic.TargetAirPurifierState.AUTO ? 'auto' : 'off';
   this.accessory.queueDataForSetAction(data);
   callback(null);
 }

154-164: ⚠️ Potential issue

Potential conflict between properties writing to the same field.

Both TargetAirPurifierStateProperty and RotationSpeedProperty write to the same field (fan_mode), but with different value types. This can lead to inconsistent device behavior depending on which property was set most recently.

Consider separating these concerns:

  1. Use different fields for different control aspects, or
  2. Implement a coordinated approach where one property updates based on the other's state.

For example:

 handleSetOn(value: CharacteristicValue, callback: CharacteristicSetCallback): void {
   const data = {};
   const speed = Math.floor((value as number));
   if (speed > 0) {
-    data['fan_mode'] = speed;
+    data['fan_speed'] = speed;
+    data['fan_mode'] = 'manual';  // Signal that we're in manual mode with a specific speed
   } else {
     data['fan_mode'] = 'off';
   }
   this.accessory.queueDataForSetAction(data);
   callback(null);
 }
🧹 Nitpick comments (3)
src/converters/air_purifier.ts (3)

186-191: Incorrect value type handling in LockPhysicalControls setter.

Similar to the TargetAirPurifierState issue, you're treating the incoming value as boolean, but HomeKit's LockPhysicalControls characteristic uses numeric constants.

 handleSetOn(value: CharacteristicValue, callback: CharacteristicSetCallback): void {
   const data = {};
-  data['child_lock'] = (value as boolean) ? 'LOCK' : 'UNLOCK';
+  data['child_lock'] = (value as number) === hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED ? 'LOCK' : 'UNLOCK';
   this.accessory.queueDataForSetAction(data);
   callback(null);
 }

55-83: Consider adding additional validation in the abstract property class.

The PassthroughAirPurifierProperty class provides a solid foundation, but adding additional validation for incoming values could improve robustness.

 updateState(state: Record<string, unknown>): void {
   if (this.expose.property in state) {
     const sensorValue = state[this.expose.property] as CharacteristicValue;
     if (sensorValue !== null && sensorValue !== undefined) {
+      // Log unexpected value types for debugging
+      const convertedValue = this.convertToAirPurifier(sensorValue);
+      if (convertedValue === undefined) {
+        this.accessory.log.debug(`Unexpected value for ${this.expose.property}: ${sensorValue}`);
+      }
-      this.state = this.convertToAirPurifier(sensorValue) ?? 0;
+      this.state = convertedValue ?? 0;
       this.service.updateCharacteristic(this.characteristic, this.state);
     }
   }
 }

224-228: Add more descriptive error for user debugging.

When no suitable properties are found, providing more specific guidance about the required properties would help users debug configuration issues.

 if (this.properties.length === 0) {
-  throw new Error(`Air Purifier (${serviceName}) did not receive any suitable exposes entries.`);
+  throw new Error(`Air Purifier (${serviceName}) did not receive any suitable exposes entries. ` + 
+    `Required properties include: fan_state, fan_mode, fan_speed, or child_lock.`);
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between 101798b and d4f0c19.

📒 Files selected for processing (1)
  • src/converters/air_purifier.ts (1 hunks)
🔇 Additional comments (3)
src/converters/air_purifier.ts (3)

1-13: LGTM - Import statements appropriately include all necessary components.

The imports are well-organized and include all the necessary components for this implementation, with clear separation between local imports and external library imports.


14-43: LGTM - Well-structured service creator with proper error handling.

The AirPurifierCreator class follows the typical pattern for service creators in the codebase, with appropriate filtering of exposes entries and proper error handling in the createService method.


194-255: LGTM - Handler implementation follows consistent pattern with good error reporting.

The AirPurifierHandler implementation follows a consistent pattern, with proper service configuration, clear property registration, and appropriate error handling. The state update mechanism is well-structured for propagating changes to all properties.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/converters/air_purifier.ts (2)

139-169: ⚠️ Potential issue

Resolve conflict between RotationSpeedProperty and TargetAirPurifierStateProperty

Both classes write to the same property 'fan_mode' but with different value types:

  • TargetAirPurifierStateProperty sets it to strings 'auto' or 'off'
  • RotationSpeedProperty sets it to either a numeric speed or the string 'off'

This will cause one property to override the other, leading to inconsistent device states.

Consider implementing a coordinator pattern or establish precedence rules for which property takes priority. For example:

  handleSetOn(value: CharacteristicValue, callback: CharacteristicSetCallback): void {
    const data = {};
    const speed = Math.floor((value as number));
    if (speed > 0) {
-     data['fan_mode'] = speed;
+     // Set both fan_mode to indicate manual mode and fan_speed for the actual speed
+     data['fan_mode'] = 'manual'; // or another appropriate value for manual mode
+     data['fan_speed'] = speed;
    } else {
      data['fan_mode'] = 'off';
    }
    this.accessory.queueDataForSetAction(data);
    callback(null);
  }

112-137: ⚠️ Potential issue

Fix boolean to numeric type handling in TargetAirPurifierStateProperty

The HomeKit spec defines TargetAirPurifierState characteristic as a numeric value where 0 = MANUAL and 1 = AUTO. However, your implementation is casting the value to boolean and then setting fan_mode to either 'off' or 'auto', which could lead to unexpected behavior.

This was previously flagged but not completely addressed.

  handleSetOn(value: CharacteristicValue, callback: CharacteristicSetCallback): void {
    const data = {};
-   data['fan_mode'] = (value as boolean) ? 'auto' : 'off';
+   // Handle correctly based on numeric HomeKit values
+   data['fan_mode'] = (value as number) === hap.Characteristic.TargetAirPurifierState.AUTO ? 'auto' : 'off';
    this.accessory.queueDataForSetAction(data);
    callback(null);
  }
🧹 Nitpick comments (3)
src/converters/air_purifier.ts (3)

230-232: Consider graceful fallback for missing entries

The handler throws an error if no suitable exposes entries are found. While this is a reasonable validation, consider implementing a fallback mechanism or more descriptive error message to help users troubleshoot the issue.

  if (this.properties.length === 0) {
-   throw new Error(`Air Purifier (${serviceName}) did not receive any suitable exposes entries.`);
+   throw new Error(`Air Purifier (${serviceName}) did not receive any suitable exposes entries. Required properties include: ${AirPurifierHandler.propertyFactories.map(f => f.name).join(', ')}`);
  }

14-43: Add logging for better debugging

Consider adding more detailed logging in the createService method to help with debugging, especially since you mentioned in the PR that this is untested code.

  private createService(endpoint: string | undefined, exposes: ExposesEntryWithProperty[], accessory: BasicAccessory): void {
    try {
+     accessory.log.debug(`Creating Air Purifier service for endpoint ${endpoint} with ${exposes.length} exposes entries`);
      const handler = new AirPurifierHandler(endpoint, exposes, accessory);
      accessory.registerServiceHandler(handler);
+     accessory.log.debug(`Successfully registered Air Purifier service for endpoint ${endpoint}`);
    } catch (error) {
      accessory.log.warn(
        'Failed to setup Air Purifier service ' + `for accessory ${accessory.displayName} for endpoint ${endpoint}: ${error}`
      );
    }
  }

171-196: Improve string value handling for LockPhysicalControlsProperty

The implementation for LockPhysicalControlsProperty has good string handling in the convert method, but the setter could be more robust by handling non-boolean inputs.

  handleSetOn(value: CharacteristicValue, callback: CharacteristicSetCallback): void {
    const data = {};
-   data['child_lock'] = (value as boolean) ? 'LOCK' : 'UNLOCK';
+   // Handle numeric values as well (0 = disabled, 1 = enabled)
+   const shouldLock = typeof value === 'boolean' ? value : 
+                      typeof value === 'number' ? value === 1 : false;
+   data['child_lock'] = shouldLock ? 'LOCK' : 'UNLOCK';
    this.accessory.queueDataForSetAction(data);
    callback(null);
  }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between d4f0c19 and 597765d.

📒 Files selected for processing (1)
  • src/converters/air_purifier.ts (1 hunks)
🔇 Additional comments (2)
src/converters/air_purifier.ts (2)

85-110: Ensure CurrentAirPurifierState is correctly synchronized with device state

The CurrentAirPurifierState property implementation looks correct for a read-only characteristic. However, you should test carefully to ensure that the device state correctly maps to the HomeKit states (INACTIVE, IDLE, PURIFYING_AIR).


1-13: Consider adding unit tests

Since you mentioned in the PR that this code is untested, I recommend adding unit tests to ensure proper functionality. This is particularly important for property conversions and state handling.

You can use the existing test infrastructure with npm run test as mentioned by user itavero in the PR comments. This would help catch issues before merging.

@Avamander Avamander changed the title [WIP]: Initial air purifier support Initial air purifier support Mar 15, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/converters/air_purifier.ts (1)

131-136: ⚠️ Potential issue

Inconsistency between HomeKit states and device commands.

The handleSetOn method casts value as boolean and maps it to either 'auto' or 'off', but HomeKit's TargetAirPurifierState is actually a numeric enum (0=MANUAL, 1=AUTO). This could lead to unexpected behavior.

  handleSetOn(value: CharacteristicValue, callback: CharacteristicSetCallback): void {
    const data = {};
-   data['fan_mode'] = (value as boolean) ? 'auto' : 'off';
+   // HomeKit TargetAirPurifierState: 0=MANUAL, 1=AUTO
+   data['fan_mode'] = (value as number) === hap.Characteristic.TargetAirPurifierState.AUTO ? 'auto' : 'off';
    this.accessory.queueDataForSetAction(data);
    callback(null);
  }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between 597765d and 9658e19.

📒 Files selected for processing (1)
  • src/converters/air_purifier.ts (1 hunks)
🔇 Additional comments (6)
src/converters/air_purifier.ts (6)

1-43: Well-implemented ServiceCreator pattern.

The AirPurifierCreator class correctly implements the ServiceCreator interface with proper error handling and endpoint grouping. The approach of filtering and transforming exposes entries aligns well with the existing patterns in the codebase.


88-90: Good fix for property type check.

The property check has been properly updated to use exposesHasProperty instead of exposesHasNumericProperty, which correctly aligns with the string-based nature of 'fan_state' values ('ON', 'OFF').


107-109: Correct implementation of CurrentAirPurifierState's handleSetOn method.

Implementing this method with a simple callback(null) is appropriate since CurrentAirPurifierState is typically read-only in HomeKit.


115-117: Good fix for property type check.

The property check has been properly updated to use exposesHasProperty instead of exposesHasNumericProperty, which correctly aligns with the string-based nature of 'fan_mode' values.


174-176: Good fix for property type check.

The property check has been properly updated to use exposesHasProperty instead of exposesHasNumericProperty, which correctly aligns with the string-based nature of 'child_lock' values ('LOCK', 'UNLOCK').


198-259: Well-structured service handler implementation.

The AirPurifierHandler class properly manages the initialization of properties, service configuration, and state updates. The error handling when no suitable properties are found is a good safeguard.

@Avamander
Copy link
Author

Avamander commented Mar 15, 2025

@itavero
I've finished the initial implementation. It works properly to control the purifier speed. I've also fixed air quality particle counts from causing exceptions, as negative particle count (-1) is returned from Zigbee2MQTT in some cases (they should be treated as undefined).

In terms of remaining snags:

  • For some reason Homebridge uses fan speeds from 0-9, HB documentation says 1-100, Z2M expects off-1-9, the device itself physically has off,auto,1-5. It's a mess. But the current implementation works with my STARKVIND properly.

  • Automatic mode is enabled over the same endpoint as fan speed, making it annoying to control.

  • Turning it off or enabling automatic mode from the toggle does not function, the exposes entries don't seem to be there.

  • Unlike Rabbit's suggestions, multiple values can't be set at the same time (which doesn't let one disable for example the child lock when the device is turned off and Z2M instead resets it to automatic mode altogether).

Unfortunately these last few snags I couldn't debug properly or they're not fixable from this side, there are way too many layers of abstraction to chew through. I'm considering it currently functionally maximally complete.

@greatestview
Copy link

@Avamander thank you! I’m looking forward to this code landing. 🥳

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants