Skip to content

Commit 1bd2cda

Browse files
authored
Add update_program service for editing program config (#388)
* Add update_program service for editing program configuration Exposes a new `bhyve.update_program` service that PUTs to `sprinkler_timer_programs/{program_id}` with only the changed fields (start_times and/or frequency). The method on BHyveProgramSwitch validates input, rejects smart programs, and requires at least one field to be provided. * Send full program payload when updating config The B-hyve API rejects partial PUTs to sprinkler_timer_programs/{id} with a 400. Start with the filtered program data (same keys used by the enabled-toggle path) and override only the fields the caller is changing.
1 parent 82d37e7 commit 1bd2cda

6 files changed

Lines changed: 342 additions & 4 deletions

File tree

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,30 @@ This integration provides the following services:
184184
| `bhyve.disable_rain_delay` | `entity_id` - device to enable a rain delay. This can reference either a zone or rain delay switch | Cancel a rain delay on a given device |
185185
| `bhyve.set_manual_preset_runtime` | `entity_id` - zone(s) entity to set the preset runtime. This should be a reference to a zone switch entity <br/> `minutes` - number of minutes to water for | Set the default time a switch is activated for when enabled. Support for this service appears to be patchy, and it has been difficult to identify the devices or under which conditions it works |
186186
| `bhyve.set_smart_watering_soil_moisture` | `entity_id` - zone(s) entity to set the moisture level for. This should be a reference to a zone switch entity <br/> `percentage` - soil moisture level between 0 - 100 | Set Smart Watering soil moisture level for a zone |
187-
| `bhyve.start_program` | `entity_id` - program entity to start. This should be a reference to a program switch entity | Starts a pre-configured watering program. Watering programs cannot be created nor configured via this integration, and require the B-Hyve app to create or modify |
187+
| `bhyve.start_program` | `entity_id` - program entity to start. This should be a reference to a program switch entity | Starts a pre-configured watering program. Watering programs cannot be created via this integration and must first be set up in the B-Hyve app |
188+
| `bhyve.update_program` | `entity_id` - program switch to update <br/> `start_times` - _(optional)_ list of watering start times in `HH:MM` format <br/> `frequency` - _(optional)_ frequency configuration object (must include a `type`, e.g. `days`, `interval`, `even`, `odd`) | Update the `start_times` and/or `frequency` of an existing non-smart program. At least one of `start_times` or `frequency` must be provided |
189+
190+
### `bhyve.update_program` example
191+
192+
```yaml
193+
service: bhyve.update_program
194+
data:
195+
entity_id: switch.front_yard_usual_programming_program
196+
start_times:
197+
- "06:00"
198+
- "18:30"
199+
frequency:
200+
type: days
201+
days: [1, 3, 5]
202+
interval: 1
203+
interval_hours: 0
204+
```
205+
206+
The `frequency` object mirrors the B-Hyve API structure. Common `type` values:
207+
208+
- `days` with `days: [0-6]` (where 0 is Sunday) to water on specific weekdays
209+
- `interval` with `interval: N` to water every N days
210+
- `even` / `odd` to water on even or odd calendar days
188211

189212
## Python Script
190213

custom_components/bhyve/services.yaml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,17 @@ start_program:
5757
fields:
5858
entity_id:
5959
description: Program
60-
example: "valve.front_yard_program"
60+
example: "valve.backyard_zone"
61+
62+
update_program:
63+
description: Update a program's configuration. Provide at least one of start_times or frequency
64+
fields:
65+
entity_id:
66+
description: Program switch
67+
example: "switch.front_yard_program"
68+
start_times:
69+
description: List of watering start times in HH:MM format
70+
example: '["06:00", "18:00"]'
71+
frequency:
72+
description: Frequency configuration. `type` is required (e.g. days, interval, even, odd)
73+
example: '{"type": "days", "days": [1, 3, 5], "interval": 1, "interval_hours": 0}'

custom_components/bhyve/strings.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,24 @@
133133
"description": "The program to start"
134134
}
135135
}
136+
},
137+
"update_program": {
138+
"name": "Update program",
139+
"description": "Update a program's configuration. Provide at least one of start_times or frequency",
140+
"fields": {
141+
"entity_id": {
142+
"name": "Program switch",
143+
"description": "The program switch to update"
144+
},
145+
"start_times": {
146+
"name": "Start times",
147+
"description": "List of watering start times in HH:MM format"
148+
},
149+
"frequency": {
150+
"name": "Frequency",
151+
"description": "Frequency configuration object. Must include a 'type' key (e.g. days, interval, even, odd)"
152+
}
153+
}
136154
}
137155
},
138156
"entity": {

custom_components/bhyve/switch.py

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,20 @@
44

55
import datetime
66
import logging
7+
import re
78
from dataclasses import dataclass
89
from datetime import timedelta
910
from typing import TYPE_CHECKING, Any
1011

12+
import voluptuous as vol
1113
from homeassistant.components.switch import (
1214
SwitchEntity,
1315
SwitchEntityDescription,
1416
)
15-
from homeassistant.const import EntityCategory
17+
from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN
18+
from homeassistant.const import ATTR_ENTITY_ID, EntityCategory
19+
from homeassistant.exceptions import ServiceValidationError
20+
from homeassistant.helpers import config_validation as cv
1621

1722
from . import BHyveCoordinatorEntity
1823
from .const import (
@@ -26,7 +31,7 @@
2631

2732
if TYPE_CHECKING:
2833
from homeassistant.config_entries import ConfigEntry
29-
from homeassistant.core import HomeAssistant
34+
from homeassistant.core import HomeAssistant, ServiceCall
3035
from homeassistant.helpers.entity_platform import AddEntitiesCallback
3136

3237
from .coordinator import BHyveDataUpdateCoordinator
@@ -78,6 +83,42 @@
7883
"start_times",
7984
}
8085

86+
# Service constants
87+
SERVICE_UPDATE_PROGRAM = "update_program"
88+
ATTR_START_TIMES = "start_times"
89+
ATTR_FREQUENCY = "frequency"
90+
91+
_TIME_RE = re.compile(r"^([01]\d|2[0-3]):[0-5]\d$")
92+
93+
94+
def _validate_time_string(value: Any) -> str:
95+
"""Validate an HH:MM time string."""
96+
if not isinstance(value, str) or not _TIME_RE.match(value):
97+
msg = f"Invalid time '{value}', expected HH:MM"
98+
raise vol.Invalid(msg)
99+
return value
100+
101+
102+
FREQUENCY_SCHEMA = vol.Schema(
103+
{
104+
vol.Required("type"): cv.string,
105+
vol.Optional("days"): [vol.All(int, vol.Range(min=0, max=6))],
106+
vol.Optional("interval"): vol.All(int, vol.Range(min=0)),
107+
vol.Optional("interval_hours"): vol.All(int, vol.Range(min=0)),
108+
},
109+
extra=vol.ALLOW_EXTRA,
110+
)
111+
112+
UPDATE_PROGRAM_SCHEMA = vol.Schema(
113+
{
114+
vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids,
115+
vol.Optional(ATTR_START_TIMES): vol.All(
116+
cv.ensure_list, [_validate_time_string]
117+
),
118+
vol.Optional(ATTR_FREQUENCY): FREQUENCY_SCHEMA,
119+
}
120+
)
121+
81122

82123
@dataclass(frozen=True, kw_only=True)
83124
class BHyveSwitchEntityDescription(SwitchEntityDescription):
@@ -170,6 +211,31 @@ async def async_setup_entry(
170211

171212
async_add_entities(switches)
172213

214+
async def async_update_program_service(call: ServiceCall) -> None:
215+
"""Handle the update_program service call."""
216+
entity_ids = call.data.get(ATTR_ENTITY_ID) or []
217+
params = {k: v for k, v in call.data.items() if k != ATTR_ENTITY_ID}
218+
219+
component = hass.data.get(SWITCH_DOMAIN)
220+
if component is None:
221+
_LOGGER.warning("Switch component not available, cannot update program")
222+
return
223+
224+
for entity_id in entity_ids:
225+
entity = component.get_entity(entity_id)
226+
if not isinstance(entity, BHyveProgramSwitch):
227+
msg = f"Entity {entity_id} is not a BHyve program switch"
228+
raise ServiceValidationError(msg)
229+
await entity.async_update_program_config(**params)
230+
231+
if not hass.services.has_service(DOMAIN, SERVICE_UPDATE_PROGRAM):
232+
hass.services.async_register(
233+
DOMAIN,
234+
SERVICE_UPDATE_PROGRAM,
235+
async_update_program_service,
236+
schema=UPDATE_PROGRAM_SCHEMA,
237+
)
238+
173239
# Listen for new programs created via WebSocket
174240
async def async_handle_program_created(event: Any) -> None:
175241
"""Handle creation of new programs."""
@@ -260,6 +326,36 @@ async def _update_program(self, *, enabled: bool) -> None:
260326
program["enabled"] = enabled
261327
await self.coordinator.client.update_program(self._program_id, program)
262328

329+
async def async_update_program_config(
330+
self,
331+
start_times: list[str] | None = None,
332+
frequency: dict | None = None,
333+
) -> None:
334+
"""Update start times and/or frequency on a non-smart program."""
335+
if self.program_data.get("is_smart_program"):
336+
msg = "Cannot update configuration of a smart program"
337+
raise ServiceValidationError(msg)
338+
339+
if start_times is None and frequency is None:
340+
msg = "At least one of start_times or frequency must be provided"
341+
raise ServiceValidationError(msg)
342+
343+
program = BHyveTimerProgram(
344+
{k: v for k, v in self.program_data.items() if k in PROGRAM_UPDATE_KEYS}
345+
)
346+
changed: list[str] = []
347+
if start_times is not None:
348+
program["start_times"] = start_times
349+
changed.append("start_times")
350+
if frequency is not None:
351+
program["frequency"] = frequency
352+
changed.append("frequency")
353+
354+
_LOGGER.info(
355+
"Updating program %s, changed fields: %s", self._program_id, changed
356+
)
357+
await self.coordinator.client.update_program(self._program_id, program)
358+
263359
async def start_program(self) -> None:
264360
"""Begins running a program."""
265361
program_payload = self.program_data.get("program")

custom_components/bhyve/translations/en.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,24 @@
133133
"description": "The program to start"
134134
}
135135
}
136+
},
137+
"update_program": {
138+
"name": "Update program",
139+
"description": "Update a program's configuration. Provide at least one of start_times or frequency",
140+
"fields": {
141+
"entity_id": {
142+
"name": "Program switch",
143+
"description": "The program switch to update"
144+
},
145+
"start_times": {
146+
"name": "Start times",
147+
"description": "List of watering start times in HH:MM format"
148+
},
149+
"frequency": {
150+
"name": "Frequency",
151+
"description": "Frequency configuration object. Must include a 'type' key (e.g. days, interval, even, odd)"
152+
}
153+
}
136154
}
137155
},
138156
"entity": {

0 commit comments

Comments
 (0)