diff --git a/README.md b/README.md index ef831d8..db37524 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 diff --git a/custom_components/aromalink_ha_integration/AromaLinkDeviceCoordinator.py b/custom_components/aromalink_ha_integration/AromaLinkDeviceCoordinator.py index 7cd2f30..3d5e57d 100644 --- a/custom_components/aromalink_ha_integration/AromaLinkDeviceCoordinator.py +++ b/custom_components/aromalink_ha_integration/AromaLinkDeviceCoordinator.py @@ -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 @@ -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) @@ -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, @@ -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 @@ -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( @@ -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}") @@ -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 diff --git a/custom_components/aromalink_ha_integration/__init__.py b/custom_components/aromalink_ha_integration/__init__.py index 2eb18a3..aa83a68 100644 --- a/custom_components/aromalink_ha_integration/__init__.py +++ b/custom_components/aromalink_ha_integration/__init__.py @@ -21,6 +21,7 @@ ATTR_WORK_DURATION, ATTR_PAUSE_DURATION, ATTR_WEEK_DAYS, + ATTR_ENABLED, ) _LOGGER = logging.getLogger(__name__) @@ -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, }) @@ -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") diff --git a/custom_components/aromalink_ha_integration/button.py b/custom_components/aromalink_ha_integration/button.py index ad77813..ac8a73f 100644 --- a/custom_components/aromalink_ha_integration/button.py +++ b/custom_components/aromalink_ha_integration/button.py @@ -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: diff --git a/custom_components/aromalink_ha_integration/const.py b/custom_components/aromalink_ha_integration/const.py index 4f01cf7..241af83 100644 --- a/custom_components/aromalink_ha_integration/const.py +++ b/custom_components/aromalink_ha_integration/const.py @@ -27,3 +27,4 @@ ATTR_WORK_DURATION = "work_duration" ATTR_PAUSE_DURATION = "pause_duration" ATTR_WEEK_DAYS = "week_days" +ATTR_ENABLED = "enabled" diff --git a/custom_components/aromalink_ha_integration/services.yaml b/custom_components/aromalink_ha_integration/services.yaml index 07ff882..80c450d 100644 --- a/custom_components/aromalink_ha_integration/services.yaml +++ b/custom_components/aromalink_ha_integration/services.yaml @@ -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"