Skip to content

feature: Reorganize controls, add API docs - #33

Merged
dalyem merged 4 commits into
dalyem:masterfrom
ndizazzo:feature/control-fixes
Jul 7, 2026
Merged

feature: Reorganize controls, add API docs#33
dalyem merged 4 commits into
dalyem:masterfrom
ndizazzo:feature/control-fixes

Conversation

@ndizazzo

@ndizazzo ndizazzo commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR re-organizes the controls to map to supported API fields, and splits the config out into a separate pane so it looks more intelligible.

Screenshot

Screenshot 2026-06-10 at 7 31 28 PM

Summary by CodeRabbit

  • New Features

    • Added independent exhaust fan switch
    • Added "Total Run Time" sensor and converted pump sensor to "Total Diffusion Time" (hours)
    • Replaced "Run" button with "Run Once" and added a settings button (marked as configuration)
  • Bug Fixes

    • Actions now validate work/pause durations (>0) and abort with a warning if invalid
  • Documentation

    • README updated with explicit Power and Fan switch descriptions
    • Added comprehensive API reference docs

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@dalyem, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 650d0532-f8a8-4b61-99e3-42818b108f0f

📥 Commits

Reviewing files that changed from the base of the PR and between 895cb02 and 391836a.

📒 Files selected for processing (2)
  • custom_components/aromalink_ha_integration/AromaLinkDeviceCoordinator.py
  • custom_components/aromalink_ha_integration/switch.py
📝 Walkthrough

Walkthrough

Splits device control into separate Power and Fan switches, adds fan state and duration validation in the coordinator, updates buttons/numbers/sensors accordingly, and adds comprehensive API documentation and README updates.

Changes

Fan Control & Platform Split

Layer / File(s) Summary
Coordinator Fan Support & Duration Validation
custom_components/aromalink_ha_integration/AromaLinkDeviceCoordinator.py
Fan field added to default state; set_fan introduced for independent exhaust fan control; _async_update_data refactored to merge runtime state from web or app sources with stale-state diagnostics; _has_valid_durations helper and run_diffuser validation ensure work/pause durations are positive before scheduling.
Switch Platform: Power & Fan Entities
custom_components/aromalink_ha_integration/switch.py
Single AromaLinkSwitch replaced with AromaLinkPowerSwitch (controls oil pumping, validates durations before turning on) and AromaLinkFanSwitch (independent exhaust fan control using coordinator.set_fan). Setup wiring now creates both entities per device.
Button & Number Platform Configuration
custom_components/aromalink_ha_integration/button.py, custom_components/aromalink_ha_integration/number.py
Run Once button replaces Run button; Run Once and Save Settings handlers default missing durations to 0 and validate both > 0 before acting. Work/pause duration number entities marked as EntityCategory.CONFIG. Duplicate setter removed from work-duration number class.
Sensor Platform: New Metrics & Repurposing
custom_components/aromalink_ha_integration/sensor.py
AromaLinkTotalRunTimeSensor added to report device runtime in hours; AromaLinkPumpCountSensor repurposed to compute "Total Diffusion Time" (pump count × work_duration) converted to rounded hours; setup replaces removed On Count sensor.
API Reference & README Updates
docs/API.md, README.md
Adds docs/API.md with a reverse-engineered Aroma-Link API reference (auth, device/state endpoints, control, scheduler, enums, response codes, capability matrix, known issues). README Entities section updated to describe separate Power and Fan switch entities and duration requirements.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant PowerUI as Power Switch UI
  participant FanUI as Fan Switch UI
  participant Coordinator as AromaLinkDeviceCoordinator
  participant Device as Aroma-Link Device
  
  User->>PowerUI: Toggle power on
  PowerUI->>PowerUI: Validate work/pause > 0
  PowerUI->>Coordinator: turn_on_off(True)
  Coordinator->>Device: POST /device/switch (onOff)
  Device->>Coordinator: state updated
  
  User->>FanUI: Toggle fan on
  FanUI->>Coordinator: set_fan(True)
  Coordinator->>Device: POST /device/switch (fan)
  Device->>Coordinator: fan flag updated
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I hopped through code with whiskers bright,
Split one switch into two by moonlight.
Fan breathes free, power pumps true,
Durations checked — no guessing through.
🐰🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: reorganizing controls into separate power and fan switches, and adding comprehensive API documentation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
custom_components/aromalink_ha_integration/AromaLinkDeviceCoordinator.py (1)

746-748: ⚡ Quick win

Improve exception chain visibility.

Consider using raise ... from e to preserve the exception chain for better debugging, as suggested by static analysis.

♻️ Proposed fix
         except Exception as e:
             _LOGGER.error(f"Error fetching device {self.device_id} info: {e}")
-            raise UpdateFailed(f"Error: {e}")
+            raise UpdateFailed(f"Error: {e}") from e
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@custom_components/aromalink_ha_integration/AromaLinkDeviceCoordinator.py`
around lines 746 - 748, The except block that catches Exception e and raises
UpdateFailed loses the original traceback; modify the error raise to preserve
the exception chain by using "raise UpdateFailed(f'Error: {e}') from e" (keep
the existing _LOGGER.error(f"Error fetching device {self.device_id} info: {e}")
line), locating this change in the exception handler inside
AromaLinkDeviceCoordinator (the block that currently does "except Exception as
e:" and calls UpdateFailed).

Source: Linters/SAST tools

custom_components/aromalink_ha_integration/switch.py (1)

50-57: 💤 Low value

Consider availability semantics and user experience.

The power switch is marked unavailable when work_duration or pause_duration are not positive. While semantically correct (the pump cannot operate without a valid schedule), users may find it confusing when the entity shows as "Unavailable" even though the device is online and reachable.

This is primarily a UX design choice. The current implementation clearly enforces the configuration constraint, though an alternative would be to keep available=True and let async_turn_on fail with a user-friendly error message explaining the missing configuration.

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

In `@custom_components/aromalink_ha_integration/switch.py` around lines 50 - 57,
The available property currently returns False when coordinator.work_duration or
coordinator.pause_duration are <= 0, causing the entity to appear "Unavailable"
even if the device is reachable; change the behavior to always return
super().available (so the entity reflects device reachability) and instead
enforce the configuration check inside async_turn_on (or the component's turn-on
handler) by validating coordinator.work_duration and coordinator.pause_duration
and raising or logging a clear, user-friendly error if they are not positive;
update symbols: the available property, coordinator.work_duration,
coordinator.pause_duration, and async_turn_on to implement this change.
custom_components/aromalink_ha_integration/sensor.py (1)

228-230: 💤 Low value

Note: Sensor returns None when work_duration is invalid.

The sensor correctly returns None when work_duration <= 0, aligning with the coordinator's duration validation logic. However, users may find it surprising that historical diffusion time data disappears when work/pause durations are configured to invalid values. This is a deliberate design choice that prevents misleading calculations, but consider documenting this behavior if users report confusion.

As per the upstream contract in AromaLinkDeviceCoordinator._has_valid_durations, work_duration must be > 0 for valid operation.

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

In `@custom_components/aromalink_ha_integration/sensor.py` around lines 228 - 230,
The sensor currently returns None when work_duration <= 0 (use the work_duration
variable in the sensor method), matching the upstream contract in
AromaLinkDeviceCoordinator._has_valid_durations; update the sensor to make this
behavior explicit by adding a short docstring or inline comment on the sensor
property/method explaining that historical diffusion time is omitted when
durations are invalid, and add a debug log (using the sensor's logger) right
before returning None to aid user debugging and visibility that work_duration
was invalid; reference the sensor property/method that reads work_duration and
AromaLinkDeviceCoordinator._has_valid_durations when making these edits.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@custom_components/aromalink_ha_integration/sensor.py`:
- Line 200: Docstrings in sensor.py use the Unicode multiplication sign "×"
which can cause encoding/readability issues; replace it with an ASCII asterisk
"*" (or 'x') in the affected docstrings—specifically the docstring that reads
"Sensor showing total diffusion time in hours (airPumpCount × work_duration)."
and the other similar docstring at the second occurrence (around line 216) so
both become "(airPumpCount * work_duration)". Locate these strings in sensor.py
and update them accordingly.

In `@docs/API.md`:
- Around line 9-30: The Table of Contents anchors are mismatched with actual
headings; update the TOC entries to use the exact heading texts: replace the
incorrect anchor for the real-time info entry with the exact heading "GET
/device/deviceInfo/now/{id}", replace the app device state anchor with "GET
/v1/app/device/newWork/{id}", update the power control anchor to match the full
heading that includes "POST /device/switch — Power On/Off and Exhaust Fan
Control", and add a new TOC entry for the existing "Schedule and Operation Mode
Endpoints" section so it appears in the TOC; ensure each TOC line uses the
literal heading text to generate correct Markdown anchors.

---

Nitpick comments:
In `@custom_components/aromalink_ha_integration/AromaLinkDeviceCoordinator.py`:
- Around line 746-748: The except block that catches Exception e and raises
UpdateFailed loses the original traceback; modify the error raise to preserve
the exception chain by using "raise UpdateFailed(f'Error: {e}') from e" (keep
the existing _LOGGER.error(f"Error fetching device {self.device_id} info: {e}")
line), locating this change in the exception handler inside
AromaLinkDeviceCoordinator (the block that currently does "except Exception as
e:" and calls UpdateFailed).

In `@custom_components/aromalink_ha_integration/sensor.py`:
- Around line 228-230: The sensor currently returns None when work_duration <= 0
(use the work_duration variable in the sensor method), matching the upstream
contract in AromaLinkDeviceCoordinator._has_valid_durations; update the sensor
to make this behavior explicit by adding a short docstring or inline comment on
the sensor property/method explaining that historical diffusion time is omitted
when durations are invalid, and add a debug log (using the sensor's logger)
right before returning None to aid user debugging and visibility that
work_duration was invalid; reference the sensor property/method that reads
work_duration and AromaLinkDeviceCoordinator._has_valid_durations when making
these edits.

In `@custom_components/aromalink_ha_integration/switch.py`:
- Around line 50-57: The available property currently returns False when
coordinator.work_duration or coordinator.pause_duration are <= 0, causing the
entity to appear "Unavailable" even if the device is reachable; change the
behavior to always return super().available (so the entity reflects device
reachability) and instead enforce the configuration check inside async_turn_on
(or the component's turn-on handler) by validating coordinator.work_duration and
coordinator.pause_duration and raising or logging a clear, user-friendly error
if they are not positive; update symbols: the available property,
coordinator.work_duration, coordinator.pause_duration, and async_turn_on to
implement this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e25e869c-ba36-48ee-b3bd-203cb2037ea8

📥 Commits

Reviewing files that changed from the base of the PR and between aa4e3c9 and 406eea9.

📒 Files selected for processing (7)
  • README.md
  • custom_components/aromalink_ha_integration/AromaLinkDeviceCoordinator.py
  • custom_components/aromalink_ha_integration/button.py
  • custom_components/aromalink_ha_integration/number.py
  • custom_components/aromalink_ha_integration/sensor.py
  • custom_components/aromalink_ha_integration/switch.py
  • docs/API.md

Comment thread custom_components/aromalink_ha_integration/sensor.py Outdated
Comment thread docs/API.md

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

🧹 Nitpick comments (1)
custom_components/aromalink_ha_integration/switch.py (1)

67-68: work_duration/pause_duration access is valid; remove duplicated duration validation
AromaLinkDeviceCoordinator defines public @property work_duration and @property pause_duration (they return the private _work_duration/_pause_duration values), so switch.py’s self.coordinator.work_duration / self.coordinator.pause_duration usage won’t raise AttributeError.

The duration checks are duplicated: AromaLinkPowerSwitch.async_turn_on validates work/pause before calling AromaLinkDeviceCoordinator.turn_on_off, and turn_on_off already guards using _has_valid_durations(). Consider delegating the validation to the coordinator to avoid repeating the same logic.

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

In `@custom_components/aromalink_ha_integration/switch.py` around lines 67 - 68,
The switch currently redundantly validates and defaults durations before calling
the coordinator; instead, remove that duplicated validation in
AromaLinkPowerSwitch.async_turn_on and pass self.coordinator.work_duration and
self.coordinator.pause_duration directly to
AromaLinkDeviceCoordinator.turn_on_off, relying on the coordinator's public
properties (work_duration, pause_duration) and its internal
_has_valid_durations()/turn_on_off checks to validate them; update async_turn_on
to stop forcing zero defaults or re-checking validity and let the coordinator
handle invalid-duration logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@custom_components/aromalink_ha_integration/switch.py`:
- Around line 67-68: The switch currently redundantly validates and defaults
durations before calling the coordinator; instead, remove that duplicated
validation in AromaLinkPowerSwitch.async_turn_on and pass
self.coordinator.work_duration and self.coordinator.pause_duration directly to
AromaLinkDeviceCoordinator.turn_on_off, relying on the coordinator's public
properties (work_duration, pause_duration) and its internal
_has_valid_durations()/turn_on_off checks to validate them; update async_turn_on
to stop forcing zero defaults or re-checking validity and let the coordinator
handle invalid-duration logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1b299111-0e11-40f3-b0ac-6f406e346297

📥 Commits

Reviewing files that changed from the base of the PR and between 406eea9 and 895cb02.

📒 Files selected for processing (4)
  • custom_components/aromalink_ha_integration/AromaLinkDeviceCoordinator.py
  • custom_components/aromalink_ha_integration/sensor.py
  • custom_components/aromalink_ha_integration/switch.py
  • docs/API.md
✅ Files skipped from review due to trivial changes (1)
  • docs/API.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • custom_components/aromalink_ha_integration/sensor.py
  • custom_components/aromalink_ha_integration/AromaLinkDeviceCoordinator.py

dalyem and others added 2 commits July 7, 2026 05:07
Merging master brought in dalyem#32's removal of the AROMA_LINK_SSL constant,
which set_fan still referenced - swap it for the auth coordinator's ssl
property. Also restore the power switch unique_id (username_deviceId_switch)
so existing entity registry entries, automations, and history survive the
Power/Fan split, and name it 'Power' to match the README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dalyem

dalyem commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Review notes

Reviewed the full diff — the Power/Fan split, duration validation, EntityCategory.CONFIG grouping, and especially docs/API.md are great additions (the API doc directly informed root-causing open issues #31 and #34).

Two fixes pushed to this branch (merge of master + 391836a):

  1. set_fan referenced AROMA_LINK_SSL, which PR fix: Update AromaLink integration for SSL bypass #32 (merged today) removed from const.py. The textual merge was clean, so this would have slipped in as a runtime NameError swallowed by the exception handler — every fan command would silently fail. Now uses ssl=self.auth_coordinator.ssl like the other call sites.
  2. Power switch unique_id continuity: the rename from {user}_{device}_switch{user}_{device}_power would orphan every existing user's power switch entity (dead registry entry, broken automations/dashboards/history). Restored the original _switch unique_id and the "Power" display name (matching this PR's own README wording); the new Fan switch keeps its new _fan unique_id.

Deliberately left as-is: replacing On Count / Pump Count with Total Run Time / Total Diffusion Time. Note this intersects open issue #2, where a user actively tracks those raw counters — I'll restore the raw count sensors alongside your derived-time sensors in a follow-up PR for that issue.

Verified: clean merge with master, all files compile, no stale constant refs (AST-checked). Merging once checks pass. Thanks @ndizazzo!

@dalyem
dalyem merged commit 67e7442 into dalyem:master Jul 7, 2026
3 checks passed
dalyem added a commit that referenced this pull request Jul 7, 2026
The control reorganization (#33) replaced the raw On Count / Pump Count
sensors with derived Total Run Time / Total Diffusion Time sensors under
new unique_ids, which orphans the counter entities that issue #2's
reporter (and anyone tracking device activity) relies on.

Bring the raw counters back alongside the derived sensors with their
original unique_ids, names, and units so existing registry entries,
dashboards, and history resume seamlessly, and mark them
TOTAL_INCREASING so Home Assistant records long-term statistics. The
class that computes Total Diffusion Time is renamed to match what it
now measures (entity-neutral; unique_ids derive from the type string).

The underlying polling was already fixed in March (counts come from the
v1 device list, which the coordinator merges on every poll); the counters
move only when the device pushes data upstream, roughly daily.

Closes #2

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dalyem added a commit that referenced this pull request Jul 14, 2026
Covers the configurable SSL bypass (#32), the Power/Fan control split
and API reference (#33), schedule disarming after momentary runs (#35),
the stale-poll shield (#36), the restored count sensors with long-term
statistics (#37), and the MIT license adoption.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants