Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,14 @@ Parameters:
- `work_duration`: Required work duration in seconds.
- `pause_duration`: Optional pause duration in seconds.
- `week_days`: Optional list of weekdays.
- `enabled`: Optional, default `true`. Set `false` to save durations while turning scheduled operation off (also handy for clearing a stuck schedule).
- `device_id`: Required when multiple devices exist.

### `aromalink_ha_integration.run_diffuser`

Run the diffuser immediately.
Run the diffuser immediately. The schedule slot used for the run is disabled
again when the run finishes, so the device does not keep cycling on its own
afterwards.

Parameters:

Expand All @@ -103,7 +106,7 @@ The integration creates:

- **Power switch** — Controls oil pumping. Requires valid work/pause durations (> 0) to activate.
- **Fan switch** — Controls the exhaust fan independently of oil pumping. Use to accelerate scent distribution without running the diffuser.
- Button entities for run/save actions
- Button entities for run/save actions. **Run Once** arms the schedule only for the duration of the run and disarms it afterwards; **Save Settings** persists the work/pause durations without enabling scheduled operation.
- Number entities for work duration, pause duration, and polling interval
- Sensor entities for runtime and device statistics

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -886,8 +886,14 @@ async def set_fan(self, state_to_set):
_LOGGER.error(f"Fan control error for device {self.device_id}: {e}")
return False

async def set_scheduler(self, work_duration=None, pause_duration=None, week_days=None):
"""Set the scheduler for the diffuser."""
async def set_scheduler(self, work_duration=None, pause_duration=None, week_days=None, enabled=True):
"""Write work/pause durations to schedule slot 0.

With enabled=True the device runs the slot as a 00:00-23:59 schedule on
the given days, which makes it auto-toggle power on its own. With
enabled=False the durations are persisted but scheduled operation stays
off, so the device only runs when commanded via the power switch.
"""
await self.auth_coordinator._ensure_login()
jsessionid = self.auth_coordinator.jsessionid

Expand All @@ -908,7 +914,7 @@ async def set_scheduler(self, work_duration=None, pause_duration=None, week_days
{
"startTime": "00:00",
"endTime": "23:59",
"enabled": 1,
"enabled": 1 if enabled else 0,
"consistenceLevel": "1",
"workDuration": str(work_duration),
"pauseDuration": str(pause_duration)
Expand Down Expand Up @@ -956,7 +962,7 @@ async def set_scheduler(self, work_duration=None, pause_duration=None, week_days
)

try:
self._log_request("POST", url, extra=f"week_days={week_days}")
self._log_request("POST", url, extra=f"week_days={week_days} enabled={enabled}")
async with self.auth_coordinator.session.post(
url,
json=payload,
Expand All @@ -971,7 +977,7 @@ async def set_scheduler(self, work_duration=None, pause_duration=None, week_days
self._work_duration = work_duration
self._pause_duration = pause_duration
_LOGGER.info(
f"Successfully set scheduler for device {self.device_id}")
f"Successfully set scheduler for device {self.device_id} (enabled={enabled})")
await self.async_request_refresh()
self.hass.async_create_task(self._delayed_refresh())
return True
Expand Down Expand Up @@ -1011,6 +1017,8 @@ async def run_diffuser(self, work_duration=None, pause_duration=None):

if not await self.turn_on_off(True):
_LOGGER.error(f"Failed to turn on device {self.device_id}")
# Don't leave the 24/7 schedule armed when the run never started.
await self._disable_schedule(current_work_duration, current_pause_duration)
return False

_LOGGER.info(
Expand All @@ -1021,6 +1029,9 @@ async def turn_off_later():
await asyncio.sleep(buffertime)
_LOGGER.info(
f"Timer complete for device {self.device_id}. Attempting to turn off.")
# Disarm the schedule first so the device cannot re-activate itself
# between the off command and the schedule write (issue #31).
await self._disable_schedule(current_work_duration, current_pause_duration)
if not await self.turn_on_off(False):
_LOGGER.error(
f"Failed to automatically turn off device {self.device_id}")
Expand All @@ -1031,3 +1042,14 @@ async def turn_off_later():
self.hass.async_create_task(turn_off_later())

return True

async def _disable_schedule(self, work_duration, pause_duration):
"""Disable schedule slot 0 while keeping the saved durations on the device."""
if await self.set_scheduler(work_duration, pause_duration, enabled=False):
_LOGGER.info(
f"Disabled schedule for device {self.device_id} after momentary run")
return True
_LOGGER.error(
f"Failed to disable schedule for device {self.device_id}; "
"the device may keep cycling until the schedule is cleared")
return False
7 changes: 5 additions & 2 deletions custom_components/aromalink_ha_integration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
ATTR_WORK_DURATION,
ATTR_PAUSE_DURATION,
ATTR_WEEK_DAYS,
ATTR_ENABLED,
)

_LOGGER = logging.getLogger(__name__)
Expand All @@ -33,6 +34,7 @@
vol.Optional(ATTR_WEEK_DAYS): vol.All(
cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(min=0, max=6))]
),
vol.Optional(ATTR_ENABLED, default=True): cv.boolean,
vol.Optional("device_id"): cv.string,
})

Expand Down Expand Up @@ -154,14 +156,15 @@ async def set_scheduler_service(call: ServiceCall):
work_duration = call.data.get(ATTR_WORK_DURATION)
pause_duration = call.data.get(ATTR_PAUSE_DURATION)
week_days = call.data.get(ATTR_WEEK_DAYS, [0, 1, 2, 3, 4, 5, 6])
enabled = call.data.get(ATTR_ENABLED, True)

# If device_id specified, use that coordinator
if device_id and device_id in device_coordinators:
await device_coordinators[device_id].set_scheduler(work_duration, pause_duration, week_days)
await device_coordinators[device_id].set_scheduler(work_duration, pause_duration, week_days, enabled=enabled)
elif len(device_coordinators) == 1:
# If only one device, use that
first_device_id = list(device_coordinators.keys())[0]
await device_coordinators[first_device_id].set_scheduler(work_duration, pause_duration, week_days)
await device_coordinators[first_device_id].set_scheduler(work_duration, pause_duration, week_days, enabled=enabled)
else:
_LOGGER.error("Multiple devices available, must specify device_id")

Expand Down
5 changes: 4 additions & 1 deletion custom_components/aromalink_ha_integration/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ async def async_press(self):
)
return

result = await self._coordinator.set_scheduler(work_duration, pause_duration)
# Persist the durations without enabling the 24/7 schedule slot, so
# saving settings no longer switches the device into scheduled
# operation (issue #31). Use the set_scheduler service to enable one.
result = await self._coordinator.set_scheduler(work_duration, pause_duration, enabled=False)
if result:
_LOGGER.info(f"Settings saved successfully for {self._coordinator.device_name}")
else:
Expand Down
1 change: 1 addition & 0 deletions custom_components/aromalink_ha_integration/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@
ATTR_WORK_DURATION = "work_duration"
ATTR_PAUSE_DURATION = "pause_duration"
ATTR_WEEK_DAYS = "week_days"
ATTR_ENABLED = "enabled"
3 changes: 3 additions & 0 deletions custom_components/aromalink_ha_integration/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ set_scheduler:
week_days:
description: "List of weekdays (0-6, where 0 is Monday) for which the schedule applies."
example: [0, 1, 2, 3, 4, 5, 6]
enabled:
description: "Whether the schedule slot is active (default true). Set false to save the durations while turning scheduled operation off, e.g. to clear a stuck schedule."
example: true
device_id:
description: "The ID of the device to apply the schedule to. Required if multiple devices are available."
example: "device_12345"
Expand Down
Loading