From e8543052b331e3180e2616c5b6e666cfc94c96c1 Mon Sep 17 00:00:00 2001 From: rgc99 Date: Sun, 24 Apr 2022 08:19:54 +0000 Subject: [PATCH] Validate zone_id's, check for duplicates & orphans --- README.md | 2 +- .../irrigation_unlimited.py | 78 +++++++++++++++++++ tests/configs/test_ids.yaml | 24 ++++++ tests/test_logging.py | 23 ++++++ 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 tests/configs/test_ids.yaml create mode 100644 tests/test_logging.py diff --git a/README.md b/README.md index a23c83d..41e103e 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ The zone object manages a collection of schedules. There must be at least one zo | Name | Type | Default | Description | | ---- | ---- | ------- | ----------- | | `schedules` | list | _[Schedule Objects](#schedule-objects)_ | Schedule details (Must have at least one) | -| `zone_id` | string | _N_ | Zone reference. Used for sequencing. | +| `zone_id` | string | _N_ | Zone reference. Used for sequencing. This should be in [snake_case](https://en.wikipedia.org/wiki/Snake_case) style | | `name` | string | Zone _N_ | Friendly name for the zone | | `enabled` | bool | true | Enable/disable the zone | | `minimum` | time | '00:01' | The minimum run time | diff --git a/custom_components/irrigation_unlimited/irrigation_unlimited.py b/custom_components/irrigation_unlimited/irrigation_unlimited.py index bf7c46f..fb15998 100644 --- a/custom_components/irrigation_unlimited/irrigation_unlimited.py +++ b/custom_components/irrigation_unlimited/irrigation_unlimited.py @@ -8,6 +8,7 @@ import uuid import time as tm import json +import re from homeassistant.core import HomeAssistant, CALLBACK_TYPE, DOMAIN as HADOMAIN from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import ( @@ -2580,6 +2581,8 @@ def load(self, config: OrderedDict) -> "IUController": sequence_config ) + self.check_links() + self._dirty = True return self @@ -2842,6 +2845,34 @@ def check_run(self, stime: datetime) -> bool: return state_changed + def check_links(self) -> bool: + """Check inter object links""" + + def check_id(zone_id: str) -> bool: + return bool(re.match(r"^[a-z0-9]+(_[a-z0-9]+)*$", zone_id)) + + result = True + zone_ids = set() + for zone in self._zones: + if not check_id(zone.zone_id): + self._coordinator.logger.log_invalid_id(self, zone) + result = False + if zone.zone_id in zone_ids: + self._coordinator.logger.log_duplicate_id(self, zone) + result = False + else: + zone_ids.add(zone.zone_id) + + for sequence in self._sequences: + for sequence_zone in sequence.zones: + for zone_id in sequence_zone.zone_ids: + if zone_id not in zone_ids: + self._coordinator.logger.log_orphan_id( + self, sequence, sequence_zone, zone_id + ) + result = False + return result + def request_update(self, deep: bool) -> None: """Flag the sensor needs an update. The actual update is done in update_sensor""" @@ -3671,6 +3702,53 @@ def log_sync_error( ), ) + def log_invalid_id( + self, controller: IUController, zone: IUZone, level=WARNING + ) -> None: + """Warn that the zone_id is not valid""" + idl = IUBase.idl([controller, zone], "0", 1) + self._output( + level, + f"INVALID_ID Invalid ID (use snake_case format): " + f"controller: {idl[0]}, " + f"zone: {idl[1]}, " + f"zone_id: {zone.zone_id}", + ) + + def log_duplicate_id( + self, controller: IUController, zone: IUZone, level=WARNING + ) -> None: + """Warn a zone has a duplicate zone_id""" + idl = IUBase.idl([controller, zone], "0", 1) + self._output( + level, + f"DUPLICATE_ID Duplicate ID: " + f"controller: {idl[0]}, " + f"zone: {idl[1]}, " + f"zone_id: {zone.zone_id}", + ) + + def log_orphan_id( + self, + controller: IUController, + sequence: IUSequence, + sequence_zone: IUSequenceZone, + zone_id: str, + level=WARNING, + ) -> None: + # pylint: disable=too-many-arguments + # pylint: disable=line-too-long + """Warn a zone_id reference is orphaned""" + idl = IUBase.idl([controller, sequence, sequence_zone], "0", 1) + self._output( + level, + f"ORPHAN_ID Invalid reference ID: " + f"controller: {idl[0]}, " + f"sequence: {idl[1]}, " + f"sequence_zone: {idl[2]}, " + f"zone_id: {zone_id}", + ) + class IUCoordinator: """Irrigation Unlimited Coordinator class""" diff --git a/tests/configs/test_ids.yaml b/tests/configs/test_ids.yaml new file mode 100644 index 0000000..1131f0e --- /dev/null +++ b/tests/configs/test_ids.yaml @@ -0,0 +1,24 @@ +default_config: + +irrigation_unlimited: + controllers: + zones: + - name: "Zone 1" + - name: "Zone 2" + zone_id: "1" + - name: "Zone 3" + zone_id: "zone_3" + - name: "Zone 4" + zone_id: "zone_3" + - name: "Zone 5" + zone_id: "ABC" + sequences: + - name: "Sequence 1" + schedules: + - name: Never + time: "21:00" + month: [feb] + day: [31] + zones: + - zone_id: 1 + - zone_id: [1, "no_zone"] diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 0000000..982b4c5 --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,23 @@ +"""irrigation_unlimited test for logging""" +# pylint: disable=unused-import +from unittest.mock import patch +import homeassistant.core as ha +from custom_components.irrigation_unlimited.irrigation_unlimited import ( + IULogger, +) +from tests.iu_test_support import IUExam + +IUExam.quiet_mode() + + +async def test_link_ids(hass: ha.HomeAssistant, skip_dependencies, skip_history): + """Test invalid, duplicate and orphaned ids.""" + # pylint: disable=unused-argument + + with patch.object(IULogger, "log_duplicate_id") as mock_duplicate: + with patch.object(IULogger, "log_invalid_id") as mock_invalid: + with patch.object(IULogger, "log_orphan_id") as mock_orphan: + async with IUExam(hass, "test_ids.yaml"): + assert mock_duplicate.call_count == 2 + assert mock_invalid.call_count == 1 + assert mock_orphan.call_count == 1