diff --git a/custom_components/irrigation_unlimited/const.py b/custom_components/irrigation_unlimited/const.py index d8323d3..caee53d 100644 --- a/custom_components/irrigation_unlimited/const.py +++ b/custom_components/irrigation_unlimited/const.py @@ -118,6 +118,7 @@ CONF_QUEUE_MANUAL = "queue_manual" CONF_USER = "user" CONF_TOGGLE = "toggle" +CONF_EXTENDED_CONFIG = "extended_config" # Defaults DEFAULT_NAME = DOMAIN diff --git a/custom_components/irrigation_unlimited/irrigation_unlimited.py b/custom_components/irrigation_unlimited/irrigation_unlimited.py index 7551919..0e5c910 100644 --- a/custom_components/irrigation_unlimited/irrigation_unlimited.py +++ b/custom_components/irrigation_unlimited/irrigation_unlimited.py @@ -209,6 +209,7 @@ CONF_QUEUE_MANUAL, CONF_USER, CONF_TOGGLE, + CONF_EXTENDED_CONFIG, ) _LOGGER: Logger = getLogger(__package__) @@ -2153,26 +2154,27 @@ def finalise(self, turn_off: bool) -> None: self.clear() self._finalised = True - def as_dict(self) -> OrderedDict: + def as_dict(self, extended=False) -> OrderedDict: """Return this zone as a dict""" - if self.runs.current_run is not None: - current_duration = self.runs.current_run.duration - else: - current_duration = timedelta(0) result = OrderedDict() result[CONF_INDEX] = self._index result[CONF_NAME] = self.name result[CONF_STATE] = STATE_ON if self.is_on else STATE_OFF result[CONF_ENABLED] = self.enabled result[ATTR_SUSPENDED] = self.suspended - result[CONF_ICON] = self.icon - result[CONF_ZONE_ID] = self._zone_id - result[CONF_ENTITY_BASE] = self.entity_base - result[ATTR_STATUS] = self.status result[ATTR_ADJUSTMENT] = str(self._adjustment) - result[ATTR_CURRENT_DURATION] = current_duration - result[CONF_SCHEDULES] = [sch.as_dict() for sch in self._schedules] - result[ATTR_SWITCH_ENTITIES] = self._switch.switch_entity_id + if extended: + if self.runs.current_run is not None: + current_duration = self.runs.current_run.duration + else: + current_duration = timedelta(0) + result[CONF_ICON] = self.icon + result[CONF_ZONE_ID] = self._zone_id + result[CONF_ENTITY_BASE] = self.entity_base + result[ATTR_STATUS] = self.status + result[ATTR_CURRENT_DURATION] = current_duration + result[CONF_SCHEDULES] = [sch.as_dict() for sch in self._schedules] + result[ATTR_SWITCH_ENTITIES] = self._switch.switch_entity_id return result def timeline(self) -> list: @@ -2510,24 +2512,31 @@ def build_zones() -> None: build_zones() return self - def as_dict(self, duration_factor: float, sqr: "IUSequenceRun" = None) -> dict: + def as_dict( + self, duration_factor: float, extended=False, sqr: "IUSequenceRun" = None + ) -> dict: """Return this sequence zone as a dict""" result = OrderedDict() result[CONF_INDEX] = self._index result[CONF_STATE] = STATE_ON if self.is_on else STATE_OFF result[CONF_ENABLED] = self.enabled result[ATTR_SUSPENDED] = self.suspended - result[CONF_ICON] = self.icon() - result[ATTR_STATUS] = self.status() - result[CONF_DELAY] = self._sequence.zone_delay(self, sqr) - result[ATTR_BASE_DURATION] = self._sequence.zone_duration_base(self, sqr) - result[ATTR_ADJUSTED_DURATION] = self._sequence.zone_duration(self, sqr) - result[ATTR_FINAL_DURATION] = self._sequence.zone_duration_final( - self, duration_factor, sqr - ) - result[CONF_ZONES] = list(zone.index + self.ZONE_OFFSET for zone in self._zones) - result[ATTR_CURRENT_DURATION] = self._sequence.runs.active_zone_duration(self) result[ATTR_ADJUSTMENT] = str(self._adjustment) + if extended: + result[CONF_ICON] = self.icon() + result[ATTR_STATUS] = self.status() + result[CONF_DELAY] = self._sequence.zone_delay(self, sqr) + result[ATTR_BASE_DURATION] = self._sequence.zone_duration_base(self, sqr) + result[ATTR_ADJUSTED_DURATION] = self._sequence.zone_duration(self, sqr) + result[ATTR_FINAL_DURATION] = self._sequence.zone_duration_final( + self, duration_factor, sqr + ) + result[CONF_ZONES] = list( + zone.index + self.ZONE_OFFSET for zone in self._zones + ) + result[ATTR_CURRENT_DURATION] = self._sequence.runs.active_zone_duration( + self + ) return result def muster(self, stime: datetime) -> IURQStatus: @@ -3657,7 +3666,7 @@ def load(self, config: OrderedDict) -> "IUSequence": self._dirty = True return self - def as_dict(self, sqr: IUSequenceRun = None) -> OrderedDict: + def as_dict(self, extended=False, sqr: IUSequenceRun = None) -> OrderedDict: """Return this sequence as a dict""" total_delay = self.total_delay(sqr) total_duration = self.total_duration(sqr) @@ -3671,20 +3680,21 @@ def as_dict(self, sqr: IUSequenceRun = None) -> OrderedDict: result[CONF_STATE] = STATE_ON if self.is_on else STATE_OFF result[CONF_ENABLED] = self._enabled result[ATTR_SUSPENDED] = self.suspended - result[ATTR_ICON] = self.icon - result[ATTR_STATUS] = self.status - result[ATTR_DEFAULT_DURATION] = self._duration - result[ATTR_DEFAULT_DELAY] = self._delay - result[ATTR_DURATION_FACTOR] = duration_factor - result[ATTR_TOTAL_DELAY] = total_delay - result[ATTR_TOTAL_DURATION] = total_duration - result[ATTR_ADJUSTED_DURATION] = total_duration_adjusted - result[ATTR_CURRENT_DURATION] = self.runs.current_duration result[ATTR_ADJUSTMENT] = str(self._adjustment) - result[CONF_SCHEDULES] = [sch.as_dict() for sch in self._schedules] result[CONF_SEQUENCE_ZONES] = [ - szn.as_dict(duration_factor) for szn in self._zones + szn.as_dict(duration_factor, extended) for szn in self._zones ] + if extended: + result[ATTR_ICON] = self.icon + result[ATTR_STATUS] = self.status + result[ATTR_DEFAULT_DURATION] = self._duration + result[ATTR_DEFAULT_DELAY] = self._delay + result[ATTR_DURATION_FACTOR] = duration_factor + result[ATTR_TOTAL_DELAY] = total_delay + result[ATTR_TOTAL_DURATION] = total_duration + result[ATTR_ADJUSTED_DURATION] = total_duration_adjusted + result[ATTR_CURRENT_DURATION] = self.runs.current_duration + result[CONF_SCHEDULES] = [sch.as_dict() for sch in self._schedules] return result def muster(self, stime: datetime) -> IURQStatus: @@ -4190,20 +4200,21 @@ def finalise(self, turn_off: bool) -> None: self.clear() self._finalised = True - def as_dict(self) -> OrderedDict: + def as_dict(self, extended=False) -> OrderedDict: """Return this controller as a dict""" result = OrderedDict() result[CONF_INDEX] = self._index result[CONF_NAME] = self._name - result[CONF_CONTROLLER_ID] = self._controller_id - result[CONF_ENTITY_BASE] = self.entity_base - result[CONF_STATE] = STATE_ON if self.is_on else STATE_OFF result[CONF_ENABLED] = self._enabled result[ATTR_SUSPENDED] = self.suspended - result[CONF_ICON] = self.icon - result[ATTR_STATUS] = self.status - result[CONF_ZONES] = [zone.as_dict() for zone in self._zones] - result[CONF_SEQUENCES] = [seq.as_dict() for seq in self._sequences] + result[CONF_ZONES] = [zone.as_dict(extended) for zone in self._zones] + result[CONF_SEQUENCES] = [seq.as_dict(extended) for seq in self._sequences] + result[CONF_STATE] = STATE_ON if self.is_on else STATE_OFF + if extended: + result[CONF_CONTROLLER_ID] = self._controller_id + result[CONF_ENTITY_BASE] = self.entity_base + result[CONF_ICON] = self.icon + result[ATTR_STATUS] = self.status return result def sequence_runs(self) -> list[IUSequenceRun]: @@ -4655,6 +4666,7 @@ def service_cancel(self, data: MappingProxyType, stime: datetime) -> bool: self.request_update(True) return changed + class IUEvent: """This class represents a single event""" @@ -5688,6 +5700,9 @@ def __init__(self, hass: HomeAssistant) -> None: self._hass = hass # Config parameters self._refresh_interval: timedelta = None + self._sync_switches: bool = True + self._rename_entities = False + self._extended_config = False # Private variables self._controllers: list[IUController] = [] self._is_on: bool = False @@ -5705,8 +5720,6 @@ def __init__(self, hass: HomeAssistant) -> None: self._clock = IUClock(self._hass, self, self._async_timer) self._history = IUHistory(self._hass, self.service_history) self._restored_from_configuration: bool = False - self._sync_switches: bool = True - self._rename_entities = False self._finalised = False @property @@ -5767,7 +5780,7 @@ def finalised(self) -> bool: @property def configuration(self) -> str: """Return the system configuration as JSON""" - return json.dumps(self.as_dict(), cls=IUJSONEncoder) + return json.dumps(self.as_dict(self._extended_config), cls=IUJSONEncoder) @property def restored_from_configuration(self) -> bool: @@ -5832,6 +5845,7 @@ def load(self, config: OrderedDict) -> "IUCoordinator": ) self._sync_switches = config.get(CONF_SYNC_SWITCHES, True) self._rename_entities = config.get(CONF_RENAME_ENTITIES, self._rename_entities) + self._extended_config = config.get(CONF_EXTENDED_CONFIG, self._extended_config) cidx: int = 0 for cidx, controller_config in enumerate(config[CONF_CONTROLLERS]): @@ -5852,11 +5866,11 @@ def load(self, config: OrderedDict) -> "IUCoordinator": return self - def as_dict(self) -> OrderedDict: + def as_dict(self, extended=False) -> OrderedDict: """Returns the coordinator as a dict""" result = OrderedDict() - result[CONF_VERSION] = "1.0.0" - result[CONF_CONTROLLERS] = [ctr.as_dict() for ctr in self._controllers] + result[CONF_VERSION] = "1.0.1" + result[CONF_CONTROLLERS] = [ctr.as_dict(extended) for ctr in self._controllers] return result def muster(self, stime: datetime, force: bool) -> IURQStatus: @@ -6182,7 +6196,7 @@ def service_call( elif zone is not None: zone.service_cancel(data1, stime) else: - changed =controller.service_cancel(data1, stime) + changed = controller.service_cancel(data1, stime) elif service == SERVICE_TIME_ADJUST: render_positive_time_period(data1, CONF_ACTUAL) render_positive_time_period(data1, CONF_INCREASE) diff --git a/custom_components/irrigation_unlimited/schema.py b/custom_components/irrigation_unlimited/schema.py index 741fcff..1a19d6e 100644 --- a/custom_components/irrigation_unlimited/schema.py +++ b/custom_components/irrigation_unlimited/schema.py @@ -90,6 +90,7 @@ CONF_QUEUE_MANUAL, CONF_USER, CONF_TOGGLE, + CONF_EXTENDED_CONFIG, ) IU_ID = r"^[a-z0-9]+(_[a-z0-9]+)*$" @@ -364,6 +365,7 @@ def _parse_dd_mmm(value: str) -> date | None: vol.Optional(CONF_TESTING): TEST_SCHEMA, vol.Optional(CONF_HISTORY): HISTORY_SCHEMA, vol.Optional(CONF_CLOCK): CLOCK_SCHEMA, + vol.Optional(CONF_EXTENDED_CONFIG): cv.boolean, } ) diff --git a/tests/configs/test_coordinator_extended.yaml b/tests/configs/test_coordinator_extended.yaml new file mode 100644 index 0000000..1570dda --- /dev/null +++ b/tests/configs/test_coordinator_extended.yaml @@ -0,0 +1,43 @@ +irrigation_unlimited: + refresh_interval: 2000 + extended_config: true + controllers: + - name: "Test controller 1" + zones: + - name: "Zone 1" + - name: "Zone 2" + - name: "Zibe 3" + enabled: false + sequences: + - name: "Sequence 1" + delay: "0:01:00" + schedules: + - time: '06:05' + weekday: [sun, wed, fri] + month: [jan, apr, jul, oct] + day: odd + zones: + - zone_id: 1 + duration: "0:06:00" + - zone_id: 2 + duration: "0:12:00" + - zone_id: 3 + duration: "0:18:00" + testing: + enabled: true + output_events: false + show_log: false + autoplay: false + times: + - name: "1-Sequence 1" + start: "2024-01-21 06:00" + end: "2024-01-21 06:30" + results: + - {t: '2024-01-21 06:05:00', c: 1, z: 0, s: 1} + - {t: '2024-01-21 06:05:00', c: 1, z: 1, s: 1} + - {t: '2024-01-21 06:11:00', c: 1, z: 1, s: 0} + - {t: '2024-01-21 06:11:00', c: 1, z: 0, s: 0} + - {t: '2024-01-21 06:12:00', c: 1, z: 0, s: 1} + - {t: '2024-01-21 06:12:00', c: 1, z: 2, s: 1} + - {t: '2024-01-21 06:24:00', c: 1, z: 2, s: 0} + - {t: '2024-01-21 06:24:00', c: 1, z: 0, s: 0} diff --git a/tests/test_coordinator_entity.py b/tests/test_coordinator_entity.py index 6e767c9..11b7043 100644 --- a/tests/test_coordinator_entity.py +++ b/tests/test_coordinator_entity.py @@ -1,4 +1,5 @@ """Test irrigation_unlimited coordinator entity operations.""" +import json from datetime import datetime import homeassistant.core as ha import homeassistant.util.dt as dt @@ -92,3 +93,52 @@ async def test_coordinator_entity( mk_local("2021-01-04 06:05:00"), ] exam.check_summary() + + +async def test_coordinator_config_essential( + hass: ha.HomeAssistant, skip_dependencies, skip_history +): + """Test coordinator configuration attribute essential keys.""" + + async with IUExam(hass, "test_coordinator_entity.yaml") as exam: + await exam.begin_test(1) + + # Check essential keys are in configuration + config = json.loads( + hass.states.get("irrigation_unlimited.coordinator").attributes[ + "configuration" + ] + ) + + controller_keys = ("state", "index", "enabled", "suspended", "name") + zone_keys = ("state", "index", "enabled", "suspended", "adjustment", "name") + sequence_keys = ("state", "index", "enabled", "suspended", "adjustment", "name") + sqz_keys = ("state", "index", "enabled", "suspended", "adjustment") + assert len(config["controllers"]) > 0 + for controller in config["controllers"]: + assert all(key in controller for key in controller_keys) + assert len(controller["zones"]) > 0 + for zone in controller["zones"]: + assert all(key in zone for key in zone_keys) + assert len(controller["sequences"]) > 0 + for sequence in controller["sequences"]: + assert all(key in sequence for key in sequence_keys) + assert len(sequence["sequence_zones"]) > 0 + for sqz in sequence["sequence_zones"]: + assert all(key in sqz for key in sqz_keys) + + await exam.finish_test() + + exam.check_summary() + + +async def test_coordinator_config_extended( + hass: ha.HomeAssistant, skip_dependencies, skip_history +): + """Test coordinator configuration attribute extended keys.""" + + async with IUExam(hass, "test_coordinator_extended.yaml") as exam: + await exam.begin_test(1) + await exam.finish_test() + + exam.check_summary()