Repository: jeroenterheerdt/HAsmartirrigation
Pull request #534: Add set_duration service. Refactor service function implementation.
Author: login: cupertinomiranda
Created: 2025-04-29T20:04:31Z | Merged: 2025-05-22T03:02:20Z

The added set_duration allows to specify the irrigation duration if the zone state is set to manual mode.
The refactoring reduces code size by merging how set_duration and set_state are implemented.
New way can likely be applied to all service handler functions if agreed as preferable.

It is even possible to improve code by making those functions be rather data driven for the general case where the input argument for the service is an integer value.

Looking forward to your review.


Diff:
diff --git a/custom_components/smart_irrigation/__init__.py b/custom_components/smart_irrigation/__init__.py
index 38213ef0..5e198910 100644
--- a/custom_components/smart_irrigation/__init__.py
+++ b/custom_components/smart_irrigation/__init__.py
@@ -4,6 +4,7 @@
 from datetime import timedelta
 import logging
 import statistics
+import re
 
 from homeassistant.components.sensor import DOMAIN as PLATFORM
 from homeassistant.config_entries import ConfigEntry
@@ -1804,51 +1805,6 @@ async def async_update_zone_config(self, zone_id: int = None, data: dict = {}):
         elif const.ATTR_UPDATE_ALL in data:
             _LOGGER.info("Updating all zones")
             await self._async_update_all()
-        elif const.ATTR_SET_MULTIPLIER in data:
-            # set a multiplier to a new vlue
-            new_multiplier_value = 0
-            if const.ATTR_NEW_MULTIPLIER_VALUE in data:
-                new_multiplier_value = data[const.ATTR_NEW_MULTIPLIER_VALUE]
-            res = self.store.get_zone(zone_id)
-            if not res:
-                return
-            data[const.ZONE_MULTIPLIER] = new_multiplier_value
-            data.pop(const.ATTR_SET_MULTIPLIER)
-            self.store.async_update_zone(zone_id, data)
-            async_dispatcher_send(self.hass, const.DOMAIN + "_config_updated", zone_id)
-        elif const.ATTR_SET_STATE in data:
-            new_state_value = "disabled"
-            if const.ATTR_NEW_STATE_VALUE in data:
-                new_state_value = data[const.ATTR_NEW_STATE_VALUE]
-            res = self.store.get_zone(zone_id)
-            if not res:
-                return
-            if not new_state_value in const.ZONE_STATES:
-                return
-            data[const.ZONE_STATE] = new_state_value
-            data.pop(const.ATTR_SET_STATE)
-            data.pop(const.ATTR_NEW_STATE_VALUE)
-            self.store.async_update_zone(zone_id, data)
-            async_dispatcher_send(self.hass, const.DOMAIN + "_config_updated", zone_id)
-        elif const.ATTR_SET_BUCKET in data:
-            # set a bucket to a new value
-            new_bucket_value = 0
-            if const.ATTR_NEW_BUCKET_VALUE in data:
-                new_bucket_value = data[const.ATTR_NEW_BUCKET_VALUE]
-            res = self.store.get_zone(zone_id)
-            if not res:
-                return
-            # apply max bucket setting
-            if (
-                const.ZONE_MAXIMUM_BUCKET in res
-                and new_bucket_value > res[const.ZONE_MAXIMUM_BUCKET]
-            ):
-                new_bucket_value = res[const.ZONE_MAXIMUM_BUCKET]
-            data[const.ZONE_BUCKET] = new_bucket_value
-            data.pop(const.ATTR_SET_BUCKET)
-
-            self.store.async_update_zone(zone_id, data)
-            async_dispatcher_send(self.hass, const.DOMAIN + "_config_updated", zone_id)
         elif const.ATTR_RESET_ALL_BUCKETS in data:
             # reset all buckets
             _LOGGER.info("Resetting all buckets")
@@ -2081,33 +2037,6 @@ async def handle_reset_all_buckets(self, call):
         _LOGGER.info("Reset all buckets service called")
         await self._async_set_all_buckets(0)
 
-    async def handle_set_bucket(self, call):
-        """Reset a specific zone bucket to new value."""
-        if (
-            const.SERVICE_ENTITY_ID in call.data
-            and const.ATTR_NEW_BUCKET_VALUE in call.data
-        ):
-            new_value = call.data[const.ATTR_NEW_BUCKET_VALUE]
-            eid = call.data[const.SERVICE_ENTITY_ID]
-            if not isinstance(eid, list):
-                eid = [call.data[const.SERVICE_ENTITY_ID]]
-            for entity in eid:
-                _LOGGER.info(
-                    "Set bucket service called for zone {}, new value: {}.".format(
-                        entity, new_value
-                    )
-                )
-                # find entity zone id and call calculate on the zone
-                state = self.hass.states.get(entity)
-                if state:
-                    # find zone_id for zone with name
-                    zone_id = state.attributes.get(const.ZONE_ID)
-                    if zone_id is not None:
-                        data = {}
-                        data[const.ATTR_SET_BUCKET] = {}
-                        data[const.ATTR_NEW_BUCKET_VALUE] = new_value
-                        await self.async_update_zone_config(zone_id=zone_id, data=data)
-
     async def handle_set_all_buckets(self, call):
         """Reset all buckets to new value."""
         if const.ATTR_NEW_BUCKET_VALUE in call.data:
@@ -2117,59 +2046,70 @@ async def handle_set_all_buckets(self, call):
             )
             await self._async_set_all_buckets(new_value)
 
-    async def handle_set_multiplier(self, call):
-        """Reset a specific zone multiplier to new value."""
-        if (
-            const.SERVICE_ENTITY_ID in call.data
-            and const.ATTR_NEW_MULTIPLIER_VALUE in call.data
-        ):
-            new_value = call.data[const.ATTR_NEW_MULTIPLIER_VALUE]
-            eid = call.data[const.SERVICE_ENTITY_ID]
-            if not isinstance(eid, list):
-                eid = [call.data[const.SERVICE_ENTITY_ID]]
-            for entity in eid:
-                _LOGGER.info(
-                    "Set multiplier service called for zone {}, new value: {}.".format(
-                        entity, new_value
-                    )
+    async def handle_set_zone(self, call):
+        """Reset a specific zone state to new value."""
+        if not const.SERVICE_ENTITY_ID in call.data:
+            return
+
+        eid = call.data[const.SERVICE_ENTITY_ID]
+        if not isinstance(eid, list):
+            eid = [call.data[const.SERVICE_ENTITY_ID]]
+
+        data = call.data.copy()
+        data.pop(const.SERVICE_ENTITY_ID)
+
+        for entity in eid:
+            _LOGGER.info(
+                "Set zone data service called with zone {}.".format(
+                    entity
                 )
-                # find entity zone id and call calculate on the zone
-                state = self.hass.states.get(entity)
-                if state:
-                    # find zone_id for zone with name
-                    zone_id = state.attributes.get(const.ZONE_ID)
-                    if zone_id is not None:
-                        data = {}
-                        data[const.ATTR_SET_MULTIPLIER] = {}
-                        data[const.ATTR_NEW_MULTIPLIER_VALUE] = new_value
-                        await self.async_update_zone_config(zone_id=zone_id, data=data)
+            )
 
-    async def handle_set_state(self, call):
-        """Reset a specific zone state to new value."""
-        if (
-            const.SERVICE_ENTITY_ID in call.data
-            and const.ATTR_NEW_STATE_VALUE in call.data
-        ):
-            new_value = call.data[const.ATTR_NEW_STATE_VALUE]
-            eid = call.data[const.SERVICE_ENTITY_ID]
-            if not isinstance(eid, list):
-                eid = [call.data[const.SERVICE_ENTITY_ID]]
-            for entity in eid:
-                _LOGGER.info(
-                    "Set state service called for zone {}, new value: {}.".format(
-                        entity, new_value
-                    )
+            # find entity zone id and call calculate on the zone
+            state = self.hass.states.get(entity)
+            if not state:
+                raise Exception("No state found for entity {}.".format(entity))
+
+            # find zone_id for zone with name
+            zone_id = state.attributes.get(const.ZONE_ID)
+            if zone_id is None:
+                raise Exception("No zone_id found in state attributes.")
+
+            zone = self.store.get_zone(zone_id)
+            zone_data = {}
+            count = 0
+            for v in data:
+                if (not v in const.LIST_SET_ZONE_ALLOWED_ARGS
+                   and v != const.SERVICE_ENTITY_ID):
+                    raise Exception("Argument ({}) is not allowed".format(v))
+
+                if (v == const.ATTR_NEW_DURATION_VALUE
+                    and zone.get(const.ZONE_STATE) != const.ZONE_STATE_MANUAL):
+                    raise Exception("Can only set duration if zone state is set to manual.")
+                if (v == const.ATTR_NEW_BUCKET_VALUE
+                    and data[v] > zone.get(const.ZONE_MAXIMUM_BUCKET)):
+                    raise Exception("Bucket size is above maximmum bucket allowed value.")
+                if (v == const.ATTR_NEW_STATE_VALUE
+                    and data[v] in const.ZONE_STATE):
+                    raise Exception("Invalid value ({}) for zone state.".format(data[v]))
+
+                m = re.match("^new_(.+)_value$", v)
+                if m:
+                    zone_data[m.group(1)] = data[v]
+                    _LOGGER.info("Setting value for {}.".format(m.group(1)))
+                    count += 1
+
+            if count == 0:
+                raise Exception("No valid parameter provided")
+
+            if count > 0:
+                self.store.async_update_zone(zone_id, zone_data)
+                async_dispatcher_send(
+                    self.hass,
+                    const.DOMAIN + "_config_updated",
+                    zone_id,
                 )
-                # find entity zone id and call calculate on the zone
-                state = self.hass.states.get(entity)
-                if state:
-                    # find zone_id for zone with name
-                    zone_id = state.attributes.get(const.ZONE_ID)
-                    if zone_id is not None:
-                        data = {}
-                        data[const.ATTR_SET_STATE] = {}
-                        data[const.ATTR_NEW_STATE_VALUE] = new_value
-                        await self.async_update_zone_config(zone_id=zone_id, data=data)
+
 
     async def handle_set_all_multipliers(self, call):
         """Reset all multipliers to new value."""
@@ -2219,7 +2159,7 @@ def register_services(hass: HomeAssistant):
     )
 
     hass.services.async_register(
-        const.DOMAIN, const.SERVICE_SET_BUCKET, coordinator.handle_set_bucket
+        const.DOMAIN, const.SERVICE_SET_BUCKET, coordinator.handle_set_zone
     )
 
     hass.services.async_register(
@@ -2239,9 +2179,9 @@ def register_services(hass: HomeAssistant):
     )
 
     hass.services.async_register(
-        const.DOMAIN, const.SERVICE_SET_MULTIPLIER, coordinator.handle_set_multiplier
+        const.DOMAIN, const.SERVICE_SET_MULTIPLIER, coordinator.handle_set_zone
     )
 
     hass.services.async_register(
-        const.DOMAIN, const.SERVICE_SET_STATE, coordinator.handle_set_state
+        const.DOMAIN, const.SERVICE_SET_ZONE, coordinator.handle_set_zone
     )
diff --git a/custom_components/smart_irrigation/const.py b/custom_components/smart_irrigation/const.py
index d91886b0..2707281a 100644
--- a/custom_components/smart_irrigation/const.py
+++ b/custom_components/smart_irrigation/const.py
@@ -95,13 +95,22 @@
 ATTR_NEW_BUCKET_VALUE = "new_bucket_value"
 ATTR_SET_MULTIPLIER = "set_multiplier"
 ATTR_NEW_MULTIPLIER_VALUE = "new_multiplier_value"
+ATTR_NEW_THROUGHPUT_VALUE = "new_throughput_value"
 ATTR_UPDATE = "update"
 ATTR_UPDATE_ALL = "update_all"
 ATTR_OVERRIDE_CACHE = "override_cache"
 ATTR_RESET_ALL_BUCKETS = "reset_all_buckets"
 ATTR_CLEAR_ALL_WEATHERDATA = "clear_all_weatherdata"
-ATTR_SET_STATE = "set_state"
 ATTR_NEW_STATE_VALUE = "new_state_value"
+ATTR_NEW_DURATION_VALUE = "new_duration_value"
+
+LIST_SET_ZONE_ALLOWED_ARGS = [
+    ATTR_NEW_BUCKET_VALUE,
+    ATTR_NEW_MULTIPLIER_VALUE,
+    ATTR_NEW_DURATION_VALUE,
+    ATTR_NEW_STATE_VALUE,
+    ATTR_NEW_THROUGHPUT_VALUE
+]
 
 ZONE_ID = "id"
 ZONE_NAME = "name"
@@ -119,6 +128,7 @@
 ZONE_DELTA = "delta"
 ZONE_EXPLANATION = "explanation"
 ZONE_MULTIPLIER = "multiplier"
+ZONE_THROUGHPUT = "throughput"
 ZONE_MAPPING = "mapping"
 ZONE_LEAD_TIME = "lead_time"
 ZONE_MAXIMUM_DURATION = "maximum_duration"
@@ -261,6 +271,7 @@
 SERVICE_SET_ALL_BUCKETS = "set_all_buckets"
 SERVICE_SET_MULTIPLIER = "set_multiplier"
 SERVICE_SET_ALL_MULTIPLIERS = "set_all_multipliers"
-SERVICE_SET_STATE = "set_state"
+SERVICE_SET_ZONE = "set_zone"
 SERVICE_ENTITY_ID = "entity_id"
 SERVICE_CLEAR_WEATHERDATA = "clear_all_weather_data"
+
diff --git a/custom_components/smart_irrigation/services.yaml b/custom_components/smart_irrigation/services.yaml
index 60509c65..43c3bee3 100644
--- a/custom_components/smart_irrigation/services.yaml
+++ b/custom_components/smart_irrigation/services.yaml
@@ -44,12 +44,28 @@ set_multiplier:
       required: true
       example: 1.0
       default: 1.0
-set_state:
+set_zone:
   target:
     entity:
       domain: sensor
   fields:
+    new_bucket_value:
+      required: false
+      example: -10
+      default: 0
+    new_multiplier_value:
+      required: false
+      example: 1.0
+      default: 1.0
+    new_duration_value:
+      required: false
+      example: 10
+      default: 0
     new_state_value:
-      required: true
+      required: false
       example: "automatic"
       default: "automatic"
+    new_throughput_value:
+      required: false
+      example: 50
+      default: 50
diff --git a/custom_components/smart_irrigation/translations/en.json b/custom_components/smart_irrigation/translations/en.json
index 891b2803..962c56b6 100644
--- a/custom_components/smart_irrigation/translations/en.json
+++ b/custom_components/smart_irrigation/translations/en.json
@@ -158,6 +158,50 @@
           "description": "New value of the state"
         }
       }
+    },
+    "set_duration": {
+      "name": "Set the duration of a zone",
+      "description": "Set the duration of a zone to a specific value, when zone is in manual mode.",
+      "fields": {
+        "entity": {
+          "name": "Entity",
+          "description": "Zone to set state for"
+        },
+        "new_duration_value": {
+          "name": "New duration value",
+          "description": "New value of the duration"
+        }
+      }
+    },
+    "set_zone": {
+      "name": "Set the values of a zone",
+      "description": "Set all configurable values of a zone",
+      "fields": {
+        "entity": {
+          "name": "Entity",
+          "description": "Zone to set configuration values"
+        },
+        "new_bucket_value": {
+          "name": "New bucket value",
+          "description": "New value for the bucket"
+        },
+        "new_multiplier_value": {
+          "name": "New multiplier value",
+          "description": "New value for the multiplier"
+        },
+        "new_duration_value": {
+          "name": "New duration value",
+          "description": "New value for the duration"
+        },
+        "new_state_value": {
+          "name": "New state value",
+          "description": "New value of the state"
+        },
+        "new_throughput_value": {
+          "name": "New throughput value",
+          "description": "New value for the throughput"
+        }
+      }
     }
   }
 }


Reviews:
user: login: cupertinomiranda
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T09:49:36Z
user: login: jeroenterheerdt
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-17T15:54:49Z

Review comments:
diff_hunk: @@ -158,6 +158,50 @@
           "description": "New value of the state"
         }
       }
+    },
+    "set_duration": {
path: custom_components/smart_irrigation/translations/en.json
user: login: cupertinomiranda
body: Oups, I think I left this entry in here by mistake
created_at: 2025-05-17T09:49:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -158,6 +158,50 @@
           "description": "New value of the state"
         }
       }
+    },
+    "set_duration": {
path: custom_components/smart_irrigation/translations/en.json
user: login: jeroenterheerdt
body: I don't have time to do this, can you please make the change and test? 
created_at: 2025-05-17T15:54:49Z
author_association: OWNER

PR comments:
user: login: jeroenterheerdt
created_at: 2025-04-30T00:08:08Z
body: please check https://github.com/jeroenterheerdt/HAsmartirrigation/actions/runs/14740280750/job/41376357026?pr=534 and add the required info.
author_association: OWNER
user: login: jeroenterheerdt
created_at: 2025-04-30T17:11:46Z
body: We should also make sure to update the docs to list the new service and any chances you are making
author_association: OWNER
user: login: cupertinomiranda
created_at: 2025-04-30T19:21:11Z
body: Would it be reasonable to replace set_duration, set_state, set_muitiplier by a set_zone service ? Can I make that change too ?
author_association: CONTRIBUTOR
user: login: cupertinomiranda
created_at: 2025-04-30T19:23:33Z
body: Regarding documentation, I honestly don't have the motivation to change documentation at this moment. Later on once development is done I can work on it. Debugging integrations in HA is bad enough. :smile: 
author_association: CONTRIBUTOR
user: login: jeroenterheerdt
created_at: 2025-04-30T19:50:34Z
body: > Would it be reasonable to replace set_duration, set_state, set_muitiplier by a set_zone service ? Can I make that change too ?

Sounds good, I'd want to keep the existing services around though so not to break people with existing installs. Code wise it can just call the same method with the right parameters.

Don't worry about docs for now we can handle it as we merge the PR 😊 Specifically we should update docs here: https://github.com/jeroenterheerdt/HAsmartirrigation/blob/master/docs/usage-services.md
author_association: OWNER
user: login: cupertinomiranda
created_at: 2025-05-02T21:30:11Z
body: Please consider current state of the pull request for review.
author_association: CONTRIBUTOR
user: login: cupertinomiranda
created_at: 2025-05-15T18:31:43Z
body: Any idea when you will be able to review this PR?
author_association: CONTRIBUTOR
user: login: jeroenterheerdt
created_at: 2025-05-17T02:11:16Z
body: > > Would it be reasonable to replace set_duration, set_state, set_muitiplier by a set_zone service ? Can I make that change too ?
> 
> Sounds good, I'd want to keep the existing services around though so not to break people with existing installs. Code wise it can just call the same method with the right parameters.
> 
> Don't worry about docs for now we can handle it as we merge the PR 😊 Specifically we should update docs here: https://github.com/jeroenterheerdt/HAsmartirrigation/blob/master/docs/usage-services.md

Did you do this @cupertinomiranda? Sorry, it's really busy here so did not check the code in detail
author_association: OWNER
user: login: cupertinomiranda
created_at: 2025-05-17T09:53:03Z
body: Hi @jeroenterheerdt,

Yes, although also refactoring the handle functions for all services which use not handle_set_zone.
I also left the original services in place, except those that I recently added as well, in the early PR.
Please notice I left some extra entry in en.yaml file. Need to change that.

Cheers,
Cupertino
author_association: CONTRIBUTOR