diff --git a/custom_components/irrigation_unlimited/irrigation_unlimited.py b/custom_components/irrigation_unlimited/irrigation_unlimited.py index 0b3abc8..a798e4d 100644 --- a/custom_components/irrigation_unlimited/irrigation_unlimited.py +++ b/custom_components/irrigation_unlimited/irrigation_unlimited.py @@ -3590,6 +3590,7 @@ def __init__( self._coordinator = coordinator self._controller = controller # Config parameters + self._sequence_id: str = None self._name: str = None self._delay: timedelta = None self._duration: timedelta = None @@ -3673,6 +3674,11 @@ def zones(self) -> list[IUSequenceZone]: """Return the list of sequence zones""" return self._zones + @property + def sequence_id(self) -> str: + """Return the id of the sequence""" + return self._sequence_id + @property def name(self) -> str: """Return the friendly name of this sequence""" @@ -3968,6 +3974,7 @@ def zone_list(self) -> Iterator[list[IUZone]]: def load(self, config: OrderedDict) -> "IUSequence": """Load sequence data from the configuration""" self.clear() + self._sequence_id = config.get(CONF_SEQUENCE_ID, str(self.index + 1)) self._name = config.get(CONF_NAME, f"Run {self.index + 1}") self._delay = wash_td(config.get(CONF_DELAY)) self._duration = wash_td(config.get(CONF_DURATION)) @@ -4804,17 +4811,22 @@ def check_run(self, stime: datetime) -> bool: def check_links(self) -> bool: """Check inter object links""" - result = True zone_ids = set() + sequence_ids = set() for zone in self._zones: if zone.zone_id in zone_ids: - self._coordinator.logger.log_duplicate_id(self, zone, None) + self._coordinator.logger.log_duplicate_id(self, zone, None, None) result = False else: zone_ids.add(zone.zone_id) for sequence in self._sequences: + if sequence.sequence_id in sequence_ids: + self._coordinator.logger.log_duplicate_id(self, None, sequence, None) + result = False + else: + sequence_ids.add(sequence.sequence_id) for sequence_zone in sequence.zones: for zone_id in sequence_zone.zone_ids: if zone_id not in zone_ids: @@ -5782,12 +5794,14 @@ def log_duplicate_id( self, controller: IUController, zone: IUZone, + sequence: IUSequence, schedule: IUSchedule, level=WARNING, ) -> None: """Warn a controller/zone/schedule has a duplicate id""" - idl = IUBase.idl([controller, zone, schedule], "0", 1) - if not zone and not schedule: + # pylint: disable=too-many-arguments + idl = IUBase.idl([controller, zone, sequence, schedule], "0", 1) + if not zone and not sequence and not schedule: self._format( level, "DUPLICATE_ID", @@ -5817,10 +5831,21 @@ def log_duplicate_id( f"controller_id: {controller.controller_id}, " f"zone: {idl[1]}, " f"zone_id: {zone.zone_id if zone else ''}, " - f"schedule: {idl[2]}, " + f"schedule: {idl[3]}, " f"schedule_id: {schedule.schedule_id if schedule else ''}", ) - else: # not zone and schedule + elif sequence and not schedule: + self._format( + level, + "DUPLICATE_ID", + None, + f"Duplicate Sequence ID: " + f"controller: {idl[0]}, " + f"controller_id: {controller.controller_id}, " + f"sequence: {idl[2]}, " + f"sequence_id: {sequence.sequence_id if sequence else ''}", + ) + elif sequence and schedule: self._format( level, "DUPLICATE_ID", @@ -5828,7 +5853,9 @@ def log_duplicate_id( f"Duplicate Schedule ID (sequence): " f"controller: {idl[0]}, " f"controller_id: {controller.controller_id}, " - f"schedule: {idl[2]}, " + f"sequence: {idl[2]}, " + f"sequence_id: {sequence.sequence_id if sequence else ''}, " + f"schedule: {idl[3]}, " f"schedule_id: {schedule.schedule_id if schedule else ''}", ) @@ -6291,7 +6318,7 @@ def check_links(self) -> bool: controller_ids = set() for controller in self._controllers: if controller.controller_id in controller_ids: - self._logger.log_duplicate_id(controller, None, None) + self._logger.log_duplicate_id(controller, None, None, None) result = False else: controller_ids.add(controller.controller_id) @@ -6304,7 +6331,9 @@ def check_links(self) -> bool: for schedule in zone.schedules: if schedule.schedule_id is not None: if schedule.schedule_id in schedule_ids: - self._logger.log_duplicate_id(controller, zone, schedule) + self._logger.log_duplicate_id( + controller, zone, None, schedule + ) result = False else: schedule_ids.add(schedule.schedule_id) @@ -6312,7 +6341,9 @@ def check_links(self) -> bool: for schedule in sequence.schedules: if schedule.schedule_id is not None: if schedule.schedule_id in schedule_ids: - self._logger.log_duplicate_id(controller, None, schedule) + self._logger.log_duplicate_id( + controller, None, sequence, schedule + ) result = False else: schedule_ids.add(schedule.schedule_id) diff --git a/custom_components/irrigation_unlimited/schema.py b/custom_components/irrigation_unlimited/schema.py index 2dc5539..6f0ec74 100644 --- a/custom_components/irrigation_unlimited/schema.py +++ b/custom_components/irrigation_unlimited/schema.py @@ -280,6 +280,7 @@ def _parse_dd_mmm(value: str) -> date | None: cv.ensure_list, [SEQUENCE_SCHEDULE_SCHEMA] ), vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_SEQUENCE_ID): cv.matches_regex(IU_ID), vol.Optional(CONF_DELAY): cv.time_period, vol.Optional(CONF_DURATION): cv.positive_time_period, vol.Optional(CONF_REPEAT): cv.positive_int, diff --git a/tests/configs/test_ids.yaml b/tests/configs/test_ids.yaml index 0b41167..df8301e 100644 --- a/tests/configs/test_ids.yaml +++ b/tests/configs/test_ids.yaml @@ -34,6 +34,7 @@ irrigation_unlimited: zone_id: "zone_3" # Conflicts with Zone 3 sequences: - name: "Sequence 1" + sequence_id: "sequence_1" schedules: - name: Never schedule_id: "schedule_2" # Conflicts with Zone 2, Schedule 1 @@ -43,6 +44,10 @@ irrigation_unlimited: zones: - zone_id: 1 - zone_id: [1, "no_zone"] # Orphaned + - name: "Sequence 2" + sequence_id: "sequence_1" # Conflicts with Sequence 1 + zones: + - zone_id: 1 - name: Controller 2 controller_id: "1" # Conflicts with controller 1 zones: diff --git a/tests/test_link_ids.py b/tests/test_link_ids.py index 36baf5d..11ab71e 100644 --- a/tests/test_link_ids.py +++ b/tests/test_link_ids.py @@ -1,4 +1,5 @@ """irrigation_unlimited test for logging""" + # pylint: disable=unused-import from unittest.mock import patch import homeassistant.core as ha @@ -17,22 +18,16 @@ async def test_link_ids(hass: ha.HomeAssistant, skip_dependencies, skip_history) with patch.object(IULogger, "log_duplicate_id") as mock_duplicate: with patch.object(IULogger, "log_orphan_id") as mock_orphan: async with IUExam(hass, "test_ids.yaml"): - assert mock_duplicate.call_count == 6 + assert mock_duplicate.call_count == 7 assert mock_orphan.call_count == 1 with patch.object(IULogger, "_format") as mock: async with IUExam(hass, "test_ids.yaml"): assert ( - sum( - [ - 1 - for call in mock.call_args_list - if call.args[1] == "DUPLICATE_ID" - ] - ) - == 6 + sum(1 for call in mock.call_args_list if call.args[1] == "DUPLICATE_ID") + == 7 ) assert ( - sum([1 for call in mock.call_args_list if call.args[1] == "ORPHAN_ID"]) + sum(1 for call in mock.call_args_list if call.args[1] == "ORPHAN_ID") == 1 )