From a5124a9e5b8fdaed6d7304d406c0edb2cf4a49dd Mon Sep 17 00:00:00 2001 From: PeterC1965 <101805108+PeterC1965@users.noreply.github.com> Date: Fri, 26 Jul 2024 08:15:50 +0100 Subject: [PATCH 1/3] Add DEM test scripts (#34234) * Add DEM test scripts * Restyled by autopep8 * Restyled by isort * Renamed DEMBaseTest.py to TC_DEMTestBase.py so that it's grouped with the TC_DEM scripts. Also split the expected result in the TestSteps() into 3rd 'expected' arg. Corrected some missing Verification steps. * Updated DEM_2.4 with corrections to match latest test plan. Also corrected PICS and description from DEM 2.3 * Restyled by isort * TC_DEM_2_5.py updated with TestSteps and corrections to match latest test plan * Reformatted TC_DEM_2_9.py * Changed PICS for DEM_2_5.py to have it as a single boolean - unsure on the correct format. * Updated DEM_2_6.py test steps in new format and corrected Test description and PICS * Fixed extra " in DEM_2_6 * Reformatted TC_DEM_2_7.py - corrected PICS and description * Corrected Feature 2 for DEM_2_6 * Corrected TC number for DEM_2_6 * Updated TC_DEM_2_8 test steps, description and pics * Restyled by isort * Apply review comments from James Harrow * Apply review comments from James Harrow * Restyled by autopep8 * Restyled by isort * Apply review comments from James Harrow * Fix python lint error * Apply review comments from James Harrow * Restyled by autopep8 * Added # === BEGIN CI TEST ARGUMENTS banners to TC_DEM python scripts * Specify --featureSet option on runner command line * Address review comments from Rob Bultman * Address review comments from Rob Bultman * Restyled by isort * Update src/python_testing/TC_DEM_2_5.py Co-authored-by: Carolina Lopes <116589288+ccruzagralopes@users.noreply.github.com> * Update TC_DEM_2_6.py - changed PICS conformance to comma separated list * Update TC_DEM_2_7.py - changed PICS conformance to comma separated list * Update TC_DEM_2_9.py - changed PICS conformance to comma separated list * Update TC_DEM_2_8.py - changed PICS conformance to comma separated list * Enabled DEM Test scripts as part of CI --------- Co-authored-by: Restyled.io Co-authored-by: James Harrow Co-authored-by: jamesharrow <93921463+jamesharrow@users.noreply.github.com> Co-authored-by: Carolina Lopes <116589288+ccruzagralopes@users.noreply.github.com> --- .github/workflows/tests.yaml | 8 + src/python_testing/TC_DEMTestBase.py | 242 ++++++++++++++++++ src/python_testing/TC_DEM_2_2.py | 11 +- src/python_testing/TC_DEM_2_3.py | 304 ++++++++++++++++++++++ src/python_testing/TC_DEM_2_4.py | 364 +++++++++++++++++++++++++++ src/python_testing/TC_DEM_2_5.py | 306 ++++++++++++++++++++++ src/python_testing/TC_DEM_2_6.py | 290 +++++++++++++++++++++ src/python_testing/TC_DEM_2_7.py | 345 +++++++++++++++++++++++++ src/python_testing/TC_DEM_2_8.py | 307 ++++++++++++++++++++++ src/python_testing/TC_DEM_2_9.py | 120 +++++++++ 10 files changed, 2290 insertions(+), 7 deletions(-) create mode 100644 src/python_testing/TC_DEMTestBase.py create mode 100644 src/python_testing/TC_DEM_2_3.py create mode 100644 src/python_testing/TC_DEM_2_4.py create mode 100644 src/python_testing/TC_DEM_2_5.py create mode 100644 src/python_testing/TC_DEM_2_6.py create mode 100644 src/python_testing/TC_DEM_2_7.py create mode 100644 src/python_testing/TC_DEM_2_8.py create mode 100644 src/python_testing/TC_DEM_2_9.py diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 592227758c035e..d17e5db8a2a541 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -513,6 +513,14 @@ jobs: scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_DRLK_2_3.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_DeviceBasicComposition.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_DeviceConformance.py' + scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_DEM_2_2.py' + scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_DEM_2_3.py' + scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_DEM_2_4.py' + scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_DEM_2_5.py' + scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_DEM_2_6.py' + scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_DEM_2_7.py' + scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_DEM_2_8.py' + scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_DEM_2_9.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_EEM_2_1.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_EEM_2_2.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_EEM_2_3.py' diff --git a/src/python_testing/TC_DEMTestBase.py b/src/python_testing/TC_DEMTestBase.py new file mode 100644 index 00000000000000..db53c36f8cd1c9 --- /dev/null +++ b/src/python_testing/TC_DEMTestBase.py @@ -0,0 +1,242 @@ +# +# Copyright (c) 2023 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import logging + +import chip.clusters as Clusters +from chip.interaction_model import InteractionModelError, Status +from matter_testing_support import utc_time_in_matter_epoch +from mobly import asserts + +logger = logging.getLogger(__name__) + + +class DEMTestBase: + + async def read_dem_attribute_expect_success(self, endpoint: int = None, attribute: str = ""): + cluster = Clusters.Objects.DeviceEnergyManagement + full_attr = getattr(cluster.Attributes, attribute) + logging.info(f"endpoint {endpoint} full_attr {full_attr}") + return await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=full_attr) + + async def check_dem_attribute(self, attribute, expected_value, endpoint: int = None): + value = await self.read_dem_attribute_expect_success(endpoint=endpoint, attribute=attribute) + asserts.assert_equal(value, expected_value, + f"Unexpected '{attribute}' value - expected {expected_value}, was {value}") + + async def send_power_adjustment_command(self, power: int, duration: int, + cause: Clusters.Objects.DeviceEnergyManagement.Enums.CauseEnum, + endpoint: int = None, timedRequestTimeoutMs: int = 3000, + expected_status: Status = Status.Success): + try: + await self.send_single_cmd(cmd=Clusters.DeviceEnergyManagement.Commands.PowerAdjustRequest( + power=power, + duration=duration, + cause=cause), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + asserts.assert_equal(expected_status, Status.Success) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + + async def send_cancel_power_adjustment_command(self, endpoint: int = None, timedRequestTimeoutMs: int = 3000, + expected_status: Status = Status.Success): + try: + await self.send_single_cmd(cmd=Clusters.DeviceEnergyManagement.Commands.CancelPowerAdjustRequest(), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + asserts.assert_equal(expected_status, Status.Success) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + + async def send_start_time_adjust_request_command(self, requestedStartTime: int, + cause: Clusters.Objects.DeviceEnergyManagement.Enums.CauseEnum, + endpoint: int = None, timedRequestTimeoutMs: int = 3000, + expected_status: Status = Status.Success): + try: + await self.send_single_cmd(cmd=Clusters.DeviceEnergyManagement.Commands.StartTimeAdjustRequest( + requestedStartTime=requestedStartTime, + cause=cause), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + asserts.assert_equal(expected_status, Status.Success) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + + async def send_start_time_adjust_clear_command(self, + endpoint: int = None, timedRequestTimeoutMs: int = 3000, + expected_status: Status = Status.Success): + try: + await self.send_single_cmd(cmd=Clusters.DeviceEnergyManagement.Commands.StartTimeAdjustClear(), # StartTimeAdjustmentClear(), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + asserts.assert_equal(expected_status, Status.Success) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + + async def send_cancel_request_command(self, + endpoint: int = None, timedRequestTimeoutMs: int = 3000, + expected_status: Status = Status.Success): + try: + await self.send_single_cmd(cmd=Clusters.DeviceEnergyManagement.Commands.CancelRequest(), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + asserts.assert_equal(expected_status, Status.Success) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + + async def send_pause_request_command(self, duration: int, cause: + Clusters.Objects.DeviceEnergyManagement.Enums.AdjustmentCauseEnum, + endpoint: int = None, timedRequestTimeoutMs: int = 3000, + expected_status: Status = Status.Success): + try: + await self.send_single_cmd(cmd=Clusters.DeviceEnergyManagement.Commands.PauseRequest( + duration=duration, + cause=cause), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + asserts.assert_equal(expected_status, Status.Success) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + + async def send_resume_request_command(self, endpoint: int = None, timedRequestTimeoutMs: int = 3000, + expected_status: Status = Status.Success): + try: + await self.send_single_cmd(cmd=Clusters.DeviceEnergyManagement.Commands.ResumeRequest(), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + asserts.assert_equal(expected_status, Status.Success) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + + async def send_modify_forecast_request_command(self, forecastID: int, + slotAdjustments: list[Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct], + cause: Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum, + endpoint: int = None, timedRequestTimeoutMs: int = 3000, + expected_status: Status = Status.Success): + try: + await self.send_single_cmd(cmd=Clusters.DeviceEnergyManagement.Commands.ModifyForecastRequest(forecastID=forecastID, + slotAdjustments=slotAdjustments, + cause=cause), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + asserts.assert_equal(expected_status, Status.Success) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + + async def send_request_constraint_based_forecast(self, constraintList: list[Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct], + cause: Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum, + endpoint: int = None, timedRequestTimeoutMs: int = 3000, + expected_status: Status = Status.Success): + try: + await self.send_single_cmd(cmd=Clusters.DeviceEnergyManagement.Commands.RequestConstraintBasedForecast(constraints=constraintList, + cause=cause), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + asserts.assert_equal(expected_status, Status.Success) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + + def print_forecast(self, forecast): + for index, slot in enumerate(forecast.slots): + logging.info( + f" [{index}] MinDuration: {slot.minDuration} MaxDuration: {slot.maxDuration} DefaultDuration: {slot.defaultDuration}") + logging.info(f" ElapseSlotTime: {slot.elapsedSlotTime} RemainingSlotTime: {slot.remainingSlotTime}") + logging.info( + f" SlotIsPausable: {slot.slotIsPausable} MinPauseDuration: {slot.minPauseDuration} MaxPauseDuration: {slot.maxPauseDuration}") + logging.info(f" ManufacturerESAState: {slot.manufacturerESAState}") + logging.info(f" NominalPower: {slot.nominalPower} MinPower: {slot.minPower} MaxPower: {slot.maxPower}") + logging.info(f" MinPowerAdjustment: {slot.minPowerAdjustment} MaxPowerAdjustment: {slot.maxPowerAdjustment}") + logging.info( + f" MinDurationAdjustment: {slot.minDurationAdjustment} MaxDurationAdjustment: {slot.maxDurationAdjustment}") + if slot.costs is not None: + for cost_index, cost in enumerate(slot): + logging.info( + f" Cost: [{cost_index}] CostType:{cost.costType} Value: {cost.value} DecimalPoints: {cost.decimalPoints} Currency: {cost.currency}") + + def get_current_utc_time_in_seconds(self): + microseconds_in_second = 1000000 + return int(utc_time_in_matter_epoch()/microseconds_in_second) + + async def send_test_event_trigger_power_adjustment(self): + await self.send_test_event_triggers(eventTrigger=0x0098000000000000) + + async def send_test_event_trigger_power_adjustment_clear(self): + await self.send_test_event_triggers(eventTrigger=0x0098000000000001) + + async def send_test_event_trigger_user_opt_out_local(self): + await self.send_test_event_triggers(eventTrigger=0x0098000000000002) + + async def send_test_event_trigger_user_opt_out_grid(self): + await self.send_test_event_triggers(eventTrigger=0x0098000000000003) + + async def send_test_event_trigger_user_opt_out_clear_all(self): + await self.send_test_event_triggers(eventTrigger=0x0098000000000004) + + async def send_test_event_trigger_start_time_adjustment(self): + await self.send_test_event_triggers(eventTrigger=0x0098000000000005) + + async def send_test_event_trigger_start_time_adjustment_clear(self): + await self.send_test_event_triggers(eventTrigger=0x0098000000000006) + + async def send_test_event_trigger_pausable(self): + await self.send_test_event_triggers(eventTrigger=0x0098000000000007) + + async def send_test_event_trigger_pausable_next_slot(self): + await self.send_test_event_triggers(eventTrigger=0x0098000000000008) + + async def send_test_event_trigger_pausable_clear(self): + await self.send_test_event_triggers(eventTrigger=0x0098000000000009) + + async def send_test_event_trigger_forecast_adjustment(self): + await self.send_test_event_triggers(eventTrigger=0x009800000000000A) + + async def send_test_event_trigger_forecast_adjustment_next_slot(self): + await self.send_test_event_triggers(eventTrigger=0x009800000000000B) + + async def send_test_event_trigger_forecast_adjustment_clear(self): + await self.send_test_event_triggers(eventTrigger=0x009800000000000C) + + async def send_test_event_trigger_constraint_based_adjustment(self): + await self.send_test_event_triggers(eventTrigger=0x009800000000000D) + + async def send_test_event_trigger_constraint_based_adjustment_clear(self): + await self.send_test_event_triggers(eventTrigger=0x009800000000000E) + + async def send_test_event_trigger_forecast(self): + await self.send_test_event_triggers(eventTrigger=0x009800000000000F) + + async def send_test_event_trigger_forecast_clear(self): + await self.send_test_event_triggers(eventTrigger=0x0098000000000010) diff --git a/src/python_testing/TC_DEM_2_2.py b/src/python_testing/TC_DEM_2_2.py index dc81cbcc0aa9d2..0c32992d934174 100644 --- a/src/python_testing/TC_DEM_2_2.py +++ b/src/python_testing/TC_DEM_2_2.py @@ -196,7 +196,7 @@ async def test_TC_DEM_2_2(self): await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut) self.step("4") - await self.send_power_adjustment_command(power=max_power, + await self.send_power_adjustment_command(power=powerAdjustCapabilityStruct.powerAdjustCapability[0].maxPower, duration=powerAdjustCapabilityStruct.powerAdjustCapability[0].minDuration, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization) @@ -218,7 +218,6 @@ async def test_TC_DEM_2_2(self): self.step("5a") powerAdjustCapabilityStruct = await self.read_dem_attribute_expect_success(attribute="PowerAdjustmentCapability") - asserts.assert_greater_equal(len(powerAdjustCapabilityStruct.powerAdjustCapability), 1) asserts.assert_equal(powerAdjustCapabilityStruct.cause, Clusters.DeviceEnergyManagement.Enums.PowerAdjustReasonEnum.kNoAdjustment) @@ -254,7 +253,7 @@ async def test_TC_DEM_2_2(self): self.step("11") start = datetime.datetime.now() - await self.send_power_adjustment_command(power=powerAdjustCapabilityStruct.powerAdjustCapability[0].maxPower, + await self.send_power_adjustment_command(power=powerAdjustCapabilityStruct.powerAdjustCapability[0].minPower, duration=min_duration, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization) @@ -262,13 +261,12 @@ async def test_TC_DEM_2_2(self): self.step("11a") powerAdjustCapabilityStruct = await self.read_dem_attribute_expect_success(attribute="PowerAdjustmentCapability") - asserts.assert_greater_equal(len(powerAdjustCapabilityStruct.powerAdjustCapability), 1) asserts.assert_equal(powerAdjustCapabilityStruct.cause, Clusters.DeviceEnergyManagement.Enums.PowerAdjustReasonEnum.kLocalOptimizationAdjustment) self.step("12") await self.send_power_adjustment_command(power=powerAdjustCapabilityStruct.powerAdjustCapability[0].maxPower, - duration=min_duration, + duration=powerAdjustCapabilityStruct.powerAdjustCapability[0].minDuration, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization) # Wait 5 seconds for an event not to be reported @@ -279,7 +277,6 @@ async def test_TC_DEM_2_2(self): self.step("12b") powerAdjustCapabilityStruct = await self.read_dem_attribute_expect_success(attribute="PowerAdjustmentCapability") - asserts.assert_greater_equal(len(powerAdjustCapabilityStruct.powerAdjustCapability), 1) asserts.assert_equal(powerAdjustCapabilityStruct.cause, Clusters.DeviceEnergyManagement.Enums.PowerAdjustReasonEnum.kGridOptimizationAdjustment) @@ -294,7 +291,7 @@ async def test_TC_DEM_2_2(self): self.step("14") await self.send_power_adjustment_command(power=max_power, - duration=max_duration, + duration=powerAdjustCapabilityStruct.powerAdjustCapability[0].maxDuration, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) diff --git a/src/python_testing/TC_DEM_2_3.py b/src/python_testing/TC_DEM_2_3.py new file mode 100644 index 00000000000000..6bdd81c710a914 --- /dev/null +++ b/src/python_testing/TC_DEM_2_3.py @@ -0,0 +1,304 @@ +# +# Copyright (c) 2024 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments +# for details about the block below. +# +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: run1 +# test-runner-run/run1/app: ${ENERGY_MANAGEMENT_APP} +# test-runner-run/run1/factoryreset: True +# test-runner-run/run1/quiet: True +# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json --enable-key 000102030405060708090a0b0c0d0e0f --featureSet 0x7a +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --hex-arg enableKey:000102030405060708090a0b0c0d0e0f --endpoint 1 --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto +# === END CI TEST ARGUMENTS === + +import logging + +import chip.clusters as Clusters +from chip.clusters.Types import NullValue +from chip.interaction_model import Status +from matter_testing_support import EventChangeCallback, MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from mobly import asserts +from TC_DEMTestBase import DEMTestBase + +logger = logging.getLogger(__name__) + + +class TC_DEM_2_3(MatterBaseTest, DEMTestBase): + """Implementation of test case TC_DEM_2_3.""" + + def desc_TC_DEM_2_3(self) -> str: + """Returns a description of this test""" + return "4.1.3. [TC-DEM-2.3] Start Time Adjustment feature functionality with DUT as Server" + + def pics_TC_DEM_2_3(self): + """Return the PICS definitions associated with this test.""" + pics = [ + "DEM.S.F03", # Depends on F03(StartTimeAdjustment) + ] + return pics + + def steps_TC_DEM_2_3(self) -> list[TestStep]: + steps = [ + TestStep("1", "Commissioning, already done", + is_commissioning=True), + TestStep("2", "TH reads TestEventTriggersEnabled attribute from General Diagnostics Cluster.", + "Verify that TestEventTriggersEnabled attribute has a value of 1 (True)"), + TestStep("3", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Start Time Adjustment Test Event", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("3a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("3b", "TH reads Forecast attribute.", + "Value has to include EarliestStartTime<=StartTime, LatestEndTime>=EndTime, and ForecastUpdateReason=Internal Optimization"), + TestStep("3c", "TH reads OptOutState attribute.", + "Verify value is 0x00 (NoOptOut)"), + TestStep("4", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Local Optimization Test Event", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("4a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("4b", "TH reads OptOutState attribute.", + "Verify value is 0x01 (LocalOptOut)"), + TestStep("5", "TH sends StartTimeAdjustRequest with RequestedStartTime=EarliestStartTime from Forecast, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("5a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("5b", "TH reads Forecast attribute.", + "Value has to be unchanged from step 3b"), + TestStep("6", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Test Event Clear", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("6a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("6b", "TH reads OptOutState attribute.", + "Verify value is 0x00 (NoOptOut)"), + TestStep("7", "TH sends StartTimeAdjustRequest with RequestedStartTime=EarliestStartTime from Forecast, Cause=LocalOptimization.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("7a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("7b", "TH reads Forecast attribute.", + "Value has to include EarliestStartTime=StartTime, LatestEndTime>=EndTime, and ForecastUpdateReason=Local Optimization"), + TestStep("8", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Local Optimization Test Event", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("8a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("8b", "TH reads OptOutState attribute.", + "Verify value is 0x01 (LocalOptOut)"), + TestStep("8c", "TH reads Forecast attribute.", + "Value has to include EarliestStartTime<=StartTime, LatestEndTime>=EndTime, and ForecastUpdateReason=Internal Optimization"), + TestStep("9", "TH sends StartTimeAdjustRequest with RequestedStartTime=StartTime+(LatestEndTime-EndTime) from Forecast, Cause=GridOptimization.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("9a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("9b", "TH reads Forecast attribute.", + "Value has to include EarliestStartTime<=StartTime, LatestEndTime=EndTime, and ForecastUpdateReason=Grid Optimization"), + TestStep("10", "TH sends CancelRequest.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("10a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("10b", "TH reads Forecast attribute.", + "Value has to include EarliestStartTime<=StartTime, LatestEndTime>=EndTime, and ForecastUpdateReason=Internal Optimization"), + TestStep("11", "TH sends StartTimeAdjustRequest with RequestedStartTime=EarliestStartTime-1 from Forecast, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("11a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("11b", "TH reads Forecast attribute.", + "Value has to include StartTime and EndTime unchanged from step 10b"), + TestStep("12", "TH sends StartTimeAdjustRequest with RequestedStartTime=StartTime+(LatestEndTime-EndTime)+1 from Forecast, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("12a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("12b", "TH reads Forecast attribute.", + "Value has to include StartTime and EndTime unchanged from step 10b"), + TestStep("13", "TH sends CancelRequest.", + "Verify DUT responds with status INVALID_IN_STATE(0xcb)"), + TestStep("14", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Start Time Adjustment Test Event Clear", + "Verify DUT responds with status SUCCESS(0x00)"), + ] + + return steps + + @async_test_body + async def test_TC_DEM_2_3(self): + + logging.info(Clusters.Objects.DeviceEnergyManagement.Attributes.FeatureMap) + + self.step("1") + # Commission DUT - already done + + # Subscribe to Events and when they are sent push them to a queue for checking later + events_callback = EventChangeCallback(Clusters.DeviceEnergyManagement) + await events_callback.start(self.default_controller, + self.dut_node_id, + self.matter_test_config.endpoint) + + self.step("2") + await self.check_test_event_triggers_enabled() + + self.step("3") + await self.send_test_event_trigger_start_time_adjustment() + + self.step("3a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("3b") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + + asserts.assert_not_equal(forecast, NullValue) + asserts.assert_less_equal(forecast.earliestStartTime, forecast.startTime, + f"Expected forecast earliestStartTime {forecast.earliestStartTime} to be <= startTime {forecast.startTime}") + asserts.assert_greater_equal(forecast.latestEndTime, forecast.endTime, + f"Expected forecast latestEndTime {forecast.latestEndTime} to be >= endTime {forecast.endTime}") + asserts.assert_equal(forecast.forecastUpdateReason, Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization, + f"Expected forecast forecastUpdateReason {forecast.forecastUpdateReason} to be == Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization") + + self.print_forecast(forecast) + + self.step("3c") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut) + + self.step("4") + await self.send_test_event_trigger_user_opt_out_local() + + self.step("4a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("4b") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kLocalOptOut) + + self.step("5") + await self.send_start_time_adjust_request_command(requestedStartTime=forecast.earliestStartTime, + cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, + expected_status=Status.ConstraintError) + + self.step("5a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("5b") + forecast2 = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast, forecast2, + f"Expected same forcast {forecast} to be == {forecast2}") + + self.step("6") + await self.send_test_event_trigger_user_opt_out_clear_all() + + self.step("6a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("6b") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut) + + self.step("7") + await self.send_start_time_adjust_request_command(requestedStartTime=forecast.earliestStartTime, + cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization) + + self.step("7a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("7b") + forecast3 = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast3.earliestStartTime, forecast3.startTime, + f"Expected earliestStartTime {forecast3.earliestStartTime} to be == startTime {forecast3.startTime}") + asserts.assert_greater_equal(forecast3.latestEndTime, forecast3.endTime, + f"Expected latestEndTime {forecast3.latestEndTime} to be >= endTime {forecast3.endTime}") + asserts.assert_equal(forecast3.forecastUpdateReason, Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kLocalOptimization, + f"Expected forecastUpdateReason {forecast3.forecastUpdateReason} to be == LocalOptimization {Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kLocalOptimization}") + + self.step("8") + await self.send_test_event_trigger_user_opt_out_local() + + self.step("8a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("8b") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kLocalOptOut) + + self.step("8c") + forecast4 = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_less_equal(forecast4.earliestStartTime, forecast4.startTime, + f"Expected earliestStartTime {forecast4.earliestStartTime} to be <= startTime {forecast4.startTime}") + asserts.assert_greater_equal(forecast4.latestEndTime, forecast4.endTime, + f"Expected forecast latestEndTime {forecast4.latestEndTime} to be >= endTime {forecast4.endTime}") + asserts.assert_equal(forecast4.forecastUpdateReason, Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization, + f"Expected forecastUpdateReason {forecast4.forecastUpdateReason} to be == InternalOptimization {Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization}") + + self.step("9") + await self.send_start_time_adjust_request_command(requestedStartTime=forecast4.startTime+forecast4.latestEndTime - forecast4.endTime, + cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization) + + self.step("9a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("9b") + forecast5 = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_less_equal(forecast5.earliestStartTime, forecast5.startTime, + f"Expected earliestStartTime {forecast5.earliestStartTime} to be <= startTime {forecast5.startTime}") + asserts.assert_equal(forecast5.latestEndTime, forecast5.endTime, + f"Expected latestEndTime {forecast5.latestEndTime} to be == endTime {forecast5.endTime}") + asserts.assert_equal(forecast5.forecastUpdateReason, Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kGridOptimization, + f"Expected forecastUpdateReason {forecast5.forecastUpdateReason} to be == GridOptimization {Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kGridOptimization}") + + self.step("10") + await self.send_cancel_request_command() + + self.step("10a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("10b") + forecast6 = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_less_equal(forecast6.earliestStartTime, forecast6.startTime, + f"Expected earliestStartTime {forecast6.earliestStartTime} to be <= startTime {forecast6.startTime}") + asserts.assert_greater_equal(forecast6.latestEndTime, forecast6.endTime, + f"Expected latestEndTime {forecast6.latestEndTime} to be >= endTime {forecast6.endTime}") + asserts.assert_equal(forecast6.forecastUpdateReason, Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization, + f"Expected forecastUpdateReason {forecast6.forecastUpdateReason} to be == InternalOptimization {Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization}") + + self.step("11") + await self.send_start_time_adjust_request_command(requestedStartTime=forecast6.earliestStartTime - 1, + cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, + expected_status=Status.ConstraintError) + self.step("11a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("11b") + forecast7 = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast6.startTime, forecast7.startTime, + f"Expected old startTime {forecast6.startTime} to be == startTime {forecast7.startTime}") + asserts.assert_equal(forecast6.endTime, forecast7.endTime, + f"Expected old endTime {forecast6.endTime} to be == endTime {forecast7.endTime}") + + self.step("12") + await self.send_start_time_adjust_request_command(requestedStartTime=forecast7.startTime+(forecast7.latestEndTime-forecast7.endTime)+1, + cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, + expected_status=Status.ConstraintError) + self.step("12a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("12b") + forecast8 = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast7.startTime, forecast8.startTime, + f"Expected old startTime {forecast7.startTime} to be == startTime {forecast8.startTime}") + asserts.assert_equal(forecast7.endTime, forecast8.endTime, + f"Expected old endTime {forecast7.endTime} to be == endTime {forecast8.endTime}") + + self.step("13") + await self.send_cancel_request_command(expected_status=Status.InvalidInState) + + self.step("14") + await self.send_test_event_trigger_start_time_adjustment_clear() + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_DEM_2_4.py b/src/python_testing/TC_DEM_2_4.py new file mode 100644 index 00000000000000..390889fc9f2d28 --- /dev/null +++ b/src/python_testing/TC_DEM_2_4.py @@ -0,0 +1,364 @@ +# +# Copyright (c) 2024 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments +# for details about the block below. +# +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: run1 +# test-runner-run/run1/app: ${ENERGY_MANAGEMENT_APP} +# test-runner-run/run1/factoryreset: True +# test-runner-run/run1/quiet: True +# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json --enable-key 000102030405060708090a0b0c0d0e0f --featureSet 0x7a +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --hex-arg enableKey:000102030405060708090a0b0c0d0e0f --endpoint 1 --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto +# === END CI TEST ARGUMENTS === + +import logging +import time + +import chip.clusters as Clusters +from chip.clusters.Types import NullValue +from chip.interaction_model import Status +from matter_testing_support import EventChangeCallback, MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from mobly import asserts +from TC_DEMTestBase import DEMTestBase + +logger = logging.getLogger(__name__) + + +class TC_DEM_2_4(MatterBaseTest, DEMTestBase): + """Implementation of test case TC_DEM_2_4.""" + + def desc_TC_DEM_2_4(self) -> str: + """Returns a description of this test""" + return "4.1.3. [TC-DEM-2.4] Pausable feature functionality with DUT as Server" + + def pics_TC_DEM_2_4(self): + """Return the PICS definitions associated with this test.""" + pics = [ + "DEM.S.F04", # Depends on F04(Pausable) + ] + return pics + + def steps_TC_DEM_2_4(self) -> list[TestStep]: + steps = [ + TestStep("1", "Commissioning, already done", + is_commissioning=True), + TestStep("2", "TH reads TestEventTriggersEnabled attribute from General Diagnostics Cluster.", + "Verify that TestEventTriggersEnabled attribute has a value of 1 (True)"), + TestStep("3", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Pausable Test Event", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("3a", "TH reads ESAState.", + "Verify value is 0x01 (Online)"), + TestStep("3b", "TH reads Forecast.", + "Value has to include IsPausable=True, slot[0].SlotIsPausable=True, slot[0].MinPauseDuration>1, slot[0].MaxPauseDuration>1, slot[1].SlotIsPausable=False, ActiveSlotNumber=0, and ForecastUpdateReason=Internal Optimization"), + TestStep("3c", "TH reads OptOutState.", + "Verify value is 0x00 (NoOptOut)"), + TestStep("4", "TH sends PauseRequest with Duration=Forecast.slots[0].MinPauseDuration-1, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("4a", "TH reads ESAState.", + "Verify value is 0x01 (Online)"), + TestStep("5", "TH sends PauseRequest with Duration=Forecast.slots[0].MaxPauseDuration+1, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("5a", "TH reads ESAState.", + "Verify value is 0x01 (Online)"), + TestStep("6", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Grid Optimization Test Event", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("6a", "TH reads ESAState.", + "Verify value is 0x01 (Online)"), + TestStep("6b", "TH reads OptOutState.", + "Verify value is 0x02 (GridOptOut)"), + TestStep("7", "TH sends PauseRequest with Duration=Forecast.slots[0].MinPauseDuration, Cause=GridOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("7a", "TH reads ESAState.", + "Verify value is 0x01 (Online)"), + TestStep("8", "TH sends PauseRequest with Duration=Forecast.slots[0].MinPauseDuration, Cause=LocalOptimization.", + "Verify DUT responds with status SUCCESS(0x00) and event DEM.S.E02(Paused) sent"), + TestStep("8a", "TH reads ESAState.", + "Verify value is 0x05 (Paused)"), + TestStep("9", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Local Optimization Test Event.", + "Verify DUT responds with status SUCCESS(0x00) and event DEM.S.E03(Resumed) sent with Cause=3 (UserOptOut)"), + TestStep("9a", "TH reads ESAState.", + "Verify value is 0x01 (Online)"), + TestStep("9b", "TH reads OptOutState.", + "Verify value is 0x03 (OptOut)"), + TestStep("9c", "TH reads Forecast.", + "Value has to include ForecastUpdateReason=Internal Optimization"), + TestStep("10", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Test Event Clear", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("10a", "TH reads ESAState.", + "Verify value is 0x01 (Online)"), + TestStep("10b", "TH reads OptOutState.", + "Verify value is 0x00 (NoOptOut)"), + TestStep("11", "TH sends PauseRequest with Duration=Forecast.slots[0].MinPauseDuration, Cause=LocalOptimization.", + "Verify DUT responds with status SUCCESS(0x00) and event DEM.S.E02(Paused) sent"), + TestStep("11a", "TH reads ESAState.", + "Verify value is 0x05 (Paused)"), + TestStep("11b", "TH reads Forecast.", + "Value has to include ForecastUpdateReason=Local Optimization"), + TestStep("12", "TH sends ResumeRequest.", + "Verify DUT responds with status SUCCESS(0x00) and event DEM.S.E03(Resumed) sent with Cause=4 (Cancelled)"), + TestStep("12a", "TH reads ESAState.", + "Verify value is 0x01 (Online)"), + TestStep("12b", "TH reads Forecast.", + "Value has to include IsPausable=True, slots[0].SlotIsPausable=True, slots[0].MinPauseDuration>1, slots[0].MaxPauseDuration>1, slots[1].SlotIsPausable=False, ActiveSlotNumber=0, ForecastUpdateReason=Internal Optimization"), + TestStep("13", "TH sends PauseRequest with Duration=Forecast.slots[0].MinPauseDuration, Cause=LocalOptimization.", + "Verify DUT responds with status SUCCESS(0x00) and event DEM.S.E02(Paused) sent"), + TestStep("13a", "TH reads ESAState.", + "Verify value is 0x05 (Paused)"), + TestStep("13b", "TH reads Forecast.", + "Value has to include ForecastUpdateReason=Local Optimization"), + TestStep("14", "TH sends ResumeRequest.", + "Verify DUT responds with status SUCCESS(0x00) and event DEM.S.E03(Resumed) sent with Cause=4 (Cancelled)"), + TestStep("14a", "TH reads ESAState.", + "Verify value is 0x01 (Online)"), + TestStep("15", "TH sends PauseRequest with Duration=Forecast.slots[0].MinPauseDuration, Cause=LocalOptimization.", + "Verify DUT responds with status SUCCESS(0x00) and event DEM.S.E02(Paused) sent"), + TestStep("15a", "TH reads ESAState.", + "Verify value is 0x05 (Paused)"), + TestStep("16", "Wait for minPauseDuration.", + "Event DEM.S.E03(Resumed) sent with Cause=0 (NormalCompletion)"), + TestStep("16a", "TH reads ESAState.", + "Verify value is 0x01 (Online)"), + TestStep("17", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Pausable Test Event Next Slot.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("17a", "TH reads ESAState.", + "Verify value is 0x01 (Online)"), + TestStep("17b", "TH reads Forecast.", + "Value has to include ActiveSlotNumber=1"), + TestStep("18", "TH sends PauseRequest with Duration=Forecast.slots[0].MinPauseDuration, Cause=LocalOptimization.", + "Verify DUT responds with status FAILURE(0x01)"), + TestStep("18a", "TH reads ESAState.", + "Verify value is 0x01 (Online)"), + TestStep("19", "TH sends ResumeRequest.", + "Verify DUT responds with status INVALID_IN_STATE(0xcb)"), + TestStep("20", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Pausable Test Event Clear.", + "Verify DUT responds with status SUCCESS(0x00)"), + ] + + return steps + + @async_test_body + async def test_TC_DEM_2_4(self): + + logging.info(Clusters.Objects.DeviceEnergyManagement.Attributes.FeatureMap) + + self.step("1") + # Commission DUT - already done + + # Subscribe to Events and when they are sent push them to a queue for checking later + events_callback = EventChangeCallback(Clusters.DeviceEnergyManagement) + await events_callback.start(self.default_controller, + self.dut_node_id, + self.matter_test_config.endpoint) + + self.step("2") + await self.check_test_event_triggers_enabled() + + self.step("3") + await self.send_test_event_trigger_pausable() + + self.step("3a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("3b") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + + asserts.assert_not_equal(forecast, NullValue) + asserts.assert_equal(forecast.isPausable, True) + asserts.assert_greater(forecast.slots[0].minPauseDuration, 1) + asserts.assert_greater(forecast.slots[0].maxPauseDuration, 1) + asserts.assert_equal(forecast.slots[0].slotIsPausable, True) + asserts.assert_equal(forecast.slots[1].slotIsPausable, False) + asserts.assert_equal(forecast.activeSlotNumber, 0) + + asserts.assert_less_equal(forecast.earliestStartTime, forecast.startTime, + f"Expected forecast earliestStartTime {forecast.earliestStartTime} to be <= startTime {forecast.startTime}") + asserts.assert_greater_equal(forecast.latestEndTime, forecast.endTime, + f"Expected forecast latestEndTime {forecast.latestEndTime} to be >= endTime {forecast.endTime}") + asserts.assert_equal(forecast.forecastUpdateReason, Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization, + f"Expected forecast forecastUpdateReason {forecast.forecastUpdateReason} to be == Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization") + self.print_forecast(forecast) + + self.step("3c") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut) + + self.step("4") + await self.send_pause_request_command(forecast.slots[0].minPauseDuration - 1, + Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, + expected_status=Status.ConstraintError) + + self.step("4a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("5") + await self.send_pause_request_command(forecast.slots[0].maxPauseDuration + 1, + Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, + expected_status=Status.ConstraintError) + + self.step("5a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("6") + await self.send_test_event_trigger_user_opt_out_grid() + + self.step("6a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("6b") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kGridOptOut) + + self.step("7") + await self.send_pause_request_command(forecast.slots[0].minPauseDuration, + Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, + expected_status=Status.ConstraintError) + + self.step("7a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("8") + await self.send_pause_request_command(forecast.slots[0].minPauseDuration, + Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization) + + event_data = events_callback.wait_for_event_report(Clusters.DeviceEnergyManagement.Events.Paused) + + self.step("8a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kPaused) + + self.step("9") + await self.send_test_event_trigger_user_opt_out_local() + event_data = events_callback.wait_for_event_report(Clusters.DeviceEnergyManagement.Events.Resumed) + asserts.assert_equal(event_data.cause, Clusters.DeviceEnergyManagement.Enums.CauseEnum.kUserOptOut) + + self.step("9a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("9b") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kOptOut) + + self.step("9c") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + + asserts.assert_not_equal(forecast, NullValue) + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization) + + self.step("10") + await self.send_test_event_trigger_user_opt_out_clear_all() + + self.step("10a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("10b") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut) + + self.step("11") + await self.send_pause_request_command(forecast.slots[0].minPauseDuration, + Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization) + event_data = events_callback.wait_for_event_report(Clusters.DeviceEnergyManagement.Events.Paused) + + self.step("11a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kPaused) + + self.step("11b") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kLocalOptimization) + + self.step("12") + await self.send_resume_request_command() + event_data = events_callback.wait_for_event_report(Clusters.DeviceEnergyManagement.Events.Resumed) + asserts.assert_equal(event_data.cause, Clusters.DeviceEnergyManagement.Enums.CauseEnum.kCancelled) + + self.step("12a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("12b") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.isPausable, True) + asserts.assert_greater(forecast.slots[0].minPauseDuration, 1) + asserts.assert_greater(forecast.slots[0].maxPauseDuration, 1) + asserts.assert_equal(forecast.slots[0].slotIsPausable, True) + asserts.assert_equal(forecast.slots[1].slotIsPausable, False) + asserts.assert_equal(forecast.activeSlotNumber, 0) + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization) + + self.step("13") + await self.send_pause_request_command(forecast.slots[0].minPauseDuration, + Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization) + event_data = events_callback.wait_for_event_report(Clusters.DeviceEnergyManagement.Events.Paused) + + self.step("13a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kPaused) + + self.step("13b") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kLocalOptimization) + + self.step("14") + await self.send_resume_request_command() + event_data = events_callback.wait_for_event_report(Clusters.DeviceEnergyManagement.Events.Resumed) + asserts.assert_equal(event_data.cause, Clusters.DeviceEnergyManagement.Enums.CauseEnum.kCancelled) + + self.step("14a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("15") + await self.send_pause_request_command(forecast.slots[0].minPauseDuration, + Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization) + event_data = events_callback.wait_for_event_report(Clusters.DeviceEnergyManagement.Events.Paused) + + self.step("15a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kPaused) + + self.step("16") + logging.info(f"Sleeping for forecast.slots[0].minPauseDuration {forecast.slots[0].minPauseDuration}s") + time.sleep(forecast.slots[0].minPauseDuration) + event_data = events_callback.wait_for_event_report(Clusters.DeviceEnergyManagement.Events.Resumed) + asserts.assert_equal(event_data.cause, Clusters.DeviceEnergyManagement.Enums.CauseEnum.kNormalCompletion) + + self.step("16a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("17") + await self.send_test_event_trigger_pausable_next_slot() + + self.step("17a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("17b") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.activeSlotNumber, 1) + + self.step("18") + await self.send_pause_request_command(forecast.slots[0].minPauseDuration, + Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, + expected_status=Status.Failure) + + self.step("18a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("19") + await self.send_resume_request_command(expected_status=Status.InvalidInState) + + self.step("20") + await self.send_test_event_trigger_user_opt_out_clear_all() + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_DEM_2_5.py b/src/python_testing/TC_DEM_2_5.py new file mode 100644 index 00000000000000..85d3cd779b22f4 --- /dev/null +++ b/src/python_testing/TC_DEM_2_5.py @@ -0,0 +1,306 @@ +# +# Copyright (c) 2024 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: disable=invalid-name + +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments +# for details about the block below. +# +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: run1 +# test-runner-run/run1/app: ${ENERGY_MANAGEMENT_APP} +# test-runner-run/run1/factoryreset: True +# test-runner-run/run1/quiet: True +# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json --enable-key 000102030405060708090a0b0c0d0e0f --featureSet 0x7a +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --hex-arg enableKey:000102030405060708090a0b0c0d0e0f --endpoint 1 --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto +# === END CI TEST ARGUMENTS === + +"""Define Matter test case TC_DEM_2_5.""" + + +import logging + +import chip.clusters as Clusters +from chip.interaction_model import Status +from matter_testing_support import EventChangeCallback, MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from mobly import asserts +from TC_DEMTestBase import DEMTestBase + +logger = logging.getLogger(__name__) + + +class TC_DEM_2_5(MatterBaseTest, DEMTestBase): + """Implementation of test case TC_DEM_2_5.""" + + def desc_TC_DEM_2_5(self) -> str: + """Return a description of this test.""" + return "4.1.3. [TC-DEM-2.5] Forecast Adjustment with Power Forecast Reporting feature functionality with DUT as Server" + + def pics_TC_DEM_2_5(self): + """Return the PICS definitions associated with this test.""" + pics = [ + # Depends on Feature 05 (ForecastAdjustment) & Feature 01 (PowerForecastReporting) + "DEM.S.F05", "DEM.S.F01" + ] + return pics + + def steps_TC_DEM_2_5(self) -> list[TestStep]: + """Execute the test steps.""" + steps = [ + TestStep("1", "Commissioning, already done", + is_commissioning=True), + TestStep("2", "TH reads TestEventTriggersEnabled attribute from General Diagnostics Cluster.", + "Verify that TestEventTriggersEnabled attribute has a value of 1 (True)"), + TestStep("3", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Forecast Adjustment Test Event", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("3a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("3b", "TH reads Forecast attribute.", + "Value has to include slots[0].MinPowerAdjustment, slots[0].MaxPowerAdjustment, slots[0].MinDurationAdjustment, slots[0].MaxDurationAdjustment"), + TestStep("3c", "TH reads OptOutState attribute.", + "Verify value is 0x00 (NoOptOut)"), + TestStep("4", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID+1, SlotAdjustments[0].{SlotIndex=0, NominalPower=Forecast.Slots[0].MinPowerAdjustment, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=GridOptimization.", + "Verify DUT responds with status FAILURE(0x01)"), + TestStep("5", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=4, NominalPower=Forecast.Slots[0].MinPowerAdjustment, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=GridOptimization.", + "Verify DUT responds with status FAILURE(0x01)"), + TestStep("6", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, NominalPower=Forecast.Slots[0].MinPowerAdjustment-1, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=GridOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("7", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, NominalPower=Forecast.Slots[0].MaxPowerAdjustment+1, Duration=Forecast.Slots[0].MinDurationAdjustment}, Cause=GridOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("8", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, NominalPower=Forecast.Slots[0].MinPowerAdjustment, Duration=Forecast.Slots[0].MaxDurationAdjustment+1}, Cause=GridOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("9", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, NominalPower=Forecast.Slots[0].MaxPowerAdjustment, Duration=Forecast.Slots[0].MinDurationAdjustment-1}, Cause=GridOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("10", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, NominalPower=Forecast.Slots[0].MinPowerAdjustment, Duration=Forecast.Slots[0].MaxDurationAdjustment}, SlotAdjustments[1].{SlotIndex=4, NominalPower=Forecast.Slots[0].MinPowerAdjustment, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=GridOptimization.", + "Verify DUT responds with status FAILURE(0x01)"), + TestStep("11", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Local Optimization Test Event", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("11a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("11b", "TH reads OptOutState attribute.", + "Verify value is 0x02 (LocalOptOut)"), + TestStep("12", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, NominalPower=Forecast.Slots[0].MinPowerAdjustment, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("13", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, NominalPower=Forecast.Slots[0].MinPowerAdjustment, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=GridOptimization.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("13a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=GridOptimization"), + TestStep("14", "TH sends CancelRequest.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("14a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=InternalOptimization"), + TestStep("15", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, NominalPower=Forecast.Slots[0].MinPowerAdjustment, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=GridOptimization.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("15a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=GridOptimization"), + TestStep("16", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Grid Optimization Test Event", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("16a", "TH reads OptOutState attribute.", + "Verify value is 0x03 (OptOut)"), + TestStep("16b", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=Internal Optimization"), + TestStep("17", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, NominalPower=Forecast.Slots[0].MinPowerAdjustment, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=GridOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("18", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Test Event Clear", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("18a", "TH reads OptOutState attribute.", + "Verify value is 0x00 (NoOptOut)"), + TestStep("19", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, NominalPower=Forecast.Slots[0].MaxPowerAdjustment, Duration=Forecast.Slots[0].MinDurationAdjustment}, Cause=LocalOptimization.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("19a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=LocalOptimization"), + TestStep("20", "TH sends CancelRequest.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("20a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=InternalOptimization"), + TestStep("21", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Forecast Adjustment Test Event Next Slot", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("22", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, NominalPower=Forecast.Slots[0].MaxPowerAdjustment, Duration=Forecast.Slots[0].MinDurationAdjustment}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("23", "TH sends CancelRequest.", + "Verify DUT responds with status INVALID_IN_STATE(0xcb)"), + TestStep("24", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Forecast Adjustment Test Event Clear", + "Verify DUT responds with status SUCCESS(0x00)"), + ] + + return steps + + @async_test_body + async def test_TC_DEM_2_5(self): + # pylint: disable=too-many-locals, too-many-statements + """Run the test steps.""" + self.step("1") + # Commission DUT - already done + + # Subscribe to Events and when they are sent push them to a queue for checking later + events_callback = EventChangeCallback(Clusters.DeviceEnergyManagement) + await events_callback.start(self.default_controller, + self.dut_node_id, + self.matter_test_config.endpoint) + + self.step("2") + await self.check_test_event_triggers_enabled() + + self.step("3") + await self.send_test_event_trigger_forecast_adjustment() + + self.step("3a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("3b") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + + asserts.assert_is_not_none(forecast.slots[0].minPowerAdjustment) + asserts.assert_is_not_none(forecast.slots[0].maxPowerAdjustment) + asserts.assert_is_not_none(forecast.slots[0].minDurationAdjustment) + asserts.assert_is_not_none(forecast.slots[0].maxDurationAdjustment) + + self.step("3c") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut) + + self.step("4") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, nominalPower=forecast.slots[0].minPowerAdjustment, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID + 1, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.Failure) + + self.step("5") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=4, nominalPower=forecast.slots[0].minPowerAdjustment, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.Failure) + + self.step("6") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, nominalPower=forecast.slots[0].minPowerAdjustment - 1, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.ConstraintError) + + self.step("7") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, nominalPower=forecast.slots[0].maxPowerAdjustment + 1, duration=forecast.slots[0].minDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.ConstraintError) + + self.step("8") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, nominalPower=forecast.slots[0].minPowerAdjustment, duration=forecast.slots[0].maxDurationAdjustment + 1)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.ConstraintError) + + self.step("9") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, nominalPower=forecast.slots[0].maxPowerAdjustment, duration=forecast.slots[0].minDurationAdjustment - 1)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.ConstraintError) + + self.step("10") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct(slotIndex=0, nominalPower=forecast.slots[0].minPowerAdjustment, duration=forecast.slots[0].maxDurationAdjustment), + Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct(slotIndex=4, nominalPower=forecast.slots[0].minPowerAdjustment, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.Failure) + + self.step("11") + await self.send_test_event_trigger_user_opt_out_local() + + self.step("11a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("11b") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kLocalOptOut) + + self.step("12") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, nominalPower=forecast.slots[0].minPowerAdjustment, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("13") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, nominalPower=forecast.slots[0].minPowerAdjustment, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.Success) + + self.step("13a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kGridOptimization) + + self.step("14") + await self.send_cancel_request_command() + + self.step("14a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization) + + self.step("15") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, nominalPower=forecast.slots[0].minPowerAdjustment, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.Success) + + self.step("15a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kGridOptimization) + + self.step("16") + await self.send_test_event_trigger_user_opt_out_grid() + + self.step("16a") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kOptOut) + + self.step("16b") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization) + + self.step("17") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, nominalPower=forecast.slots[0].minPowerAdjustment, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.ConstraintError) + + self.step("18") + await self.send_test_event_trigger_user_opt_out_clear_all() + + self.step("18a") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut) + + self.step("19") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, nominalPower=forecast.slots[0].minPowerAdjustment, duration=forecast.slots[0].minDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.Success) + + self.step("19a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kLocalOptimization) + + self.step("20") + await self.send_cancel_request_command() + + self.step("20a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization) + + self.step("21") + await self.send_test_event_trigger_forecast_adjustment_next_slot() + + self.step("22") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, nominalPower=forecast.slots[0].minPowerAdjustment, duration=forecast.slots[0].minDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("23") + await self.send_cancel_request_command(expected_status=Status.InvalidInState) + + self.step("24") + await self.send_test_event_trigger_forecast_adjustment_clear() + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_DEM_2_6.py b/src/python_testing/TC_DEM_2_6.py new file mode 100644 index 00000000000000..7390436effd937 --- /dev/null +++ b/src/python_testing/TC_DEM_2_6.py @@ -0,0 +1,290 @@ +# +# Copyright (c) 2024 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: disable=invalid-name + +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments +# for details about the block below. +# +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: run1 +# test-runner-run/run1/app: ${ENERGY_MANAGEMENT_APP} +# test-runner-run/run1/factoryreset: True +# test-runner-run/run1/quiet: True +# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json --enable-key 000102030405060708090a0b0c0d0e0f --featureSet 0x7c +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --hex-arg enableKey:000102030405060708090a0b0c0d0e0f --endpoint 1 --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto +# === END CI TEST ARGUMENTS === + +"""Define Matter test case TC_DEM_2_6.""" + + +import logging + +import chip.clusters as Clusters +from chip.interaction_model import Status +from matter_testing_support import EventChangeCallback, MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from mobly import asserts +from TC_DEMTestBase import DEMTestBase + +logger = logging.getLogger(__name__) + + +class TC_DEM_2_6(MatterBaseTest, DEMTestBase): + """Implementation of test case TC_DEM_2_6.""" + + def desc_TC_DEM_2_6(self) -> str: + """Return a description of this test.""" + return "4.1.3. [TC-DEM-2.6] Forecast Adjustment with State Forecast Reporting feature functionality with DUT as Server" + + def pics_TC_DEM_2_6(self): + """Return the PICS definitions associated with this test.""" + pics = [ + # Depends on Feature 05 (ForecastAdjustment) & Feature 02 (StateForecastReporting) + "DEM.S.F05", "DEM.S.F02" + ] + return pics + + def steps_TC_DEM_2_6(self) -> list[TestStep]: + """Execute the test steps.""" + steps = [ + TestStep("1", "Commissioning, already done", + is_commissioning=True), + TestStep("2", "TH reads TestEventTriggersEnabled attribute from General Diagnostics Cluster.", + "Verify that TestEventTriggersEnabled attribute has a value of 1 (True)"), + TestStep("3", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Forecast Adjustment Test Event", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("3a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("3b", "TH reads Forecast attribute.", + "Value has to include slots[0].MinDurationAdjustment, slots[0].MaxDurationAdjustment"), + TestStep("3c", "TH reads OptOutState attribute.", + "Verify value is 0x00 (NoOptOut)"), + TestStep("4", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID+1, SlotAdjustments[0].{SlotIndex=0, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=GridOptimization.", + "Verify DUT responds with status FAILURE(0x01)"), + TestStep("5", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=4, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=GridOptimization.", + "Verify DUT responds with status FAILURE(0x01)"), + TestStep("6", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, Duration=Forecast.Slots[0].MaxDurationAdjustment+1}, Cause=GridOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("7", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, Duration=Forecast.Slots[0].MinDurationAdjustment-1}, Cause=GridOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("8", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, Duration=Forecast.Slots[0].MaxDurationAdjustment}, SlotAdjustments[1].{SlotIndex=4, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=GridOptimization.", + "Verify DUT responds with status FAILURE(0x01)"), + TestStep("9", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Local Optimization Test Event", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("9a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("9b", "TH reads OptOutState attribute.", + "Verify value is 0x02 (LocalOptOut)"), + TestStep("10", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("11", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=GridOptimization.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("11a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=GridOptimization"), + TestStep("12", "TH sends CancelRequest.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("12a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=InternalOptimization"), + TestStep("13", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=GridOptimization.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("13a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=GridOptimization"), + TestStep("14", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Grid Optimization Test Event", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("14a", "TH reads OptOutState attribute.", + "Verify value is 0x03 (OptOut)"), + TestStep("14b", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=Internal Optimization"), + TestStep("15", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, Duration=Forecast.Slots[0].MaxDurationAdjustment}, Cause=GridOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("16", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Test Event Clear", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("16a", "TH reads OptOutState attribute.", + "Verify value is 0x00 (NoOptOut)"), + TestStep("17", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, Duration=Forecast.Slots[0].MinDurationAdjustment}, Cause=LocalOptimization.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("17a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=LocalOptimization"), + TestStep("18", "TH sends CancelRequest.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("18a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=InternalOptimization"), + TestStep("19", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Forecast Adjustment Test Event Next Slot", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("20", "TH sends ModifyForecastRequest with ForecastID=Forecast.ForecastID, SlotAdjustments[0].{SlotIndex=0, Duration=Forecast.Slots[0].MinDurationAdjustment}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("21", "TH sends CancelRequest.", + "Verify DUT responds with status INVALID_IN_STATE(0xcb)"), + TestStep("22", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Forecast Adjustment Test Event Clear", + "Verify DUT responds with status SUCCESS(0x00)"), + ] + + return steps + + @async_test_body + async def test_TC_DEM_2_6(self): + # pylint: disable=too-many-locals, too-many-statements + """Run the test steps.""" + self.step("1") + # Commission DUT - already done + + # Subscribe to Events and when they are sent push them to a queue for checking later + events_callback = EventChangeCallback(Clusters.DeviceEnergyManagement) + await events_callback.start(self.default_controller, + self.dut_node_id, + self.matter_test_config.endpoint) + + self.step("2") + await self.check_test_event_triggers_enabled() + + self.step("3") + await self.send_test_event_trigger_forecast_adjustment() + + self.step("3a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("3b") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_is_not_none(forecast.slots[0].minDurationAdjustment) + asserts.assert_is_not_none(forecast.slots[0].maxDurationAdjustment) + + self.step("3c") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut) + + self.step("4") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID + 1, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.Failure) + + self.step("5") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=4, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.Failure) + + self.step("6") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, duration=forecast.slots[0].maxDurationAdjustment + 1)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.ConstraintError) + + self.step("7") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, duration=forecast.slots[0].minDurationAdjustment - 1)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.ConstraintError) + + self.step("8") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct(slotIndex=0, duration=forecast.slots[0].maxDurationAdjustment), + Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct(slotIndex=4, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.Failure) + + self.step("9") + await self.send_test_event_trigger_user_opt_out_local() + + self.step("9a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("9b") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kLocalOptOut) + + self.step("10") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("11") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.Success) + + self.step("11a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + logging.info(forecast) + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kGridOptimization) + + self.step("12") + await self.send_cancel_request_command() + + self.step("12a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization) + + self.step("13") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.Success) + + self.step("13a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kGridOptimization) + + self.step("14") + await self.send_test_event_trigger_user_opt_out_grid() + + self.step("14a") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kOptOut) + + self.step("14b") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization) + + self.step("15") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, duration=forecast.slots[0].maxDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.ConstraintError) + + self.step("16") + await self.send_test_event_trigger_user_opt_out_clear_all() + + self.step("16a") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut) + + self.step("17") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, duration=forecast.slots[0].minDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.Success) + + self.step("17a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kLocalOptimization) + + self.step("18") + await self.send_cancel_request_command() + + self.step("18a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization) + + self.step("19") + await self.send_test_event_trigger_forecast_adjustment_next_slot() + + self.step("20") + slotAdjustments = [Clusters.DeviceEnergyManagement.Structs.SlotAdjustmentStruct( + slotIndex=0, duration=forecast.slots[0].minDurationAdjustment)] + await self.send_modify_forecast_request_command(forecast.forecastID, slotAdjustments, Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("21") + await self.send_cancel_request_command(expected_status=Status.InvalidInState) + + self.step("22") + await self.send_test_event_trigger_forecast_adjustment_clear() + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_DEM_2_7.py b/src/python_testing/TC_DEM_2_7.py new file mode 100644 index 00000000000000..fa7dba14a53315 --- /dev/null +++ b/src/python_testing/TC_DEM_2_7.py @@ -0,0 +1,345 @@ +# +# Copyright (c) 2024 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: disable=invalid-name + +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments +# for details about the block below. +# +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: run1 +# test-runner-run/run1/app: ${ENERGY_MANAGEMENT_APP} +# test-runner-run/run1/factoryreset: True +# test-runner-run/run1/quiet: True +# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json --enable-key 000102030405060708090a0b0c0d0e0f --featureSet 0x7a +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --hex-arg enableKey:000102030405060708090a0b0c0d0e0f --endpoint 1 --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto +# === END CI TEST ARGUMENTS === + +"""Define Matter test case TC_DEM_2_7.""" + + +import logging + +import chip.clusters as Clusters +from chip.interaction_model import Status +from matter_testing_support import EventChangeCallback, MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from mobly import asserts +from TC_DEMTestBase import DEMTestBase + +logger = logging.getLogger(__name__) + + +class TC_DEM_2_7(MatterBaseTest, DEMTestBase): + """Implementation of test case TC_DEM_2_7.""" + + def desc_TC_DEM_2_7(self) -> str: + """Return a description of this test.""" + return "4.1.3. [TC-DEM-2.7] Constraints-based Adjustment with Power Forecast Reporting feature functionality with DUT as Server" + + def pics_TC_DEM_2_7(self): + """Return the PICS definitions associated with this test.""" + pics = [ + # Depends on Feature 06 (ConstraintBasedAdjustment) & Feature 01 (PowerForecastReporting) + "DEM.S.F06", "DEM.S.F01" + ] + return pics + + def steps_TC_DEM_2_7(self) -> list[TestStep]: + """Execute the test steps.""" + steps = [ + TestStep("1", "Commissioning, already done", + is_commissioning=True), + TestStep("2", "TH reads TestEventTriggersEnabled attribute from General Diagnostics Cluster.", + "Verify value is 1 (True)"), + TestStep("3", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Constraints-based Adjustment Test Event.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("3a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("3b", "TH reads Forecast attribute.", + "Value has to include valid slots[0].NominalPower, slots[0].MinPower, slots[0].MaxPower, slots[0].NominalEnergy"), + TestStep("3c", "TH reads OptOutState attribute.", + "Verify value is 0x00 (NoOptOut)"), + TestStep("4", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=now()-10, Duration=20, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("5", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=now()+10, Duration=20, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, constraints[1].{StartTime=now()+20, Duration=20, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, constraints[2].{StartTime=now()+40, Duration=20, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("6", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=now()+10, Duration=20, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, constraints[1].{StartTime=now()+30, Duration=20, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, constraints[2].{StartTime=now()+40, Duration=20, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("7", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=now()+30, Duration=20, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, constraints[1].{StartTime=now()+10, Duration=20, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, constraints[2].{StartTime=now()+50, Duration=20, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("8", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=now()+10, Duration=20, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, constraints[1].{StartTime=now()+50, Duration=20, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, constraints[2].{StartTime=now()+30, Duration=20, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("9", "TH reads AbsMaxPower attribute attribute.", + "Save the value"), + TestStep("9a", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, NominalPower=AbsMaxPower+1, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("10", "TH reads AbsMinPower attribute attribute.", + "Save the value"), + TestStep("10a", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, NominalPower=AbsMinPower-1, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("11", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, NominalPower=Forecast.Slots[0].NominalPower}, Cause=LocalOptimization.", + "Verify DUT responds with status InvalidCommand"), + TestStep("12", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, Cause=LocalOptimization.", + "Verify DUT responds with status InvalidCommand"), + TestStep("13", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Local Optimization Test Event.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("13a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("13b", "TH reads OptOutState attribute.", + "Verify value is 0x02 (LocalOptOut)"), + TestStep("14", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("15", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, Cause=GridOptimization.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("15a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=GridOptimization"), + TestStep("16", "TH sends CancelRequest.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("16a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=InternalOptimization"), + TestStep("17", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, Cause=GridOptimization.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("17a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=GridOptimization"), + TestStep("18", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Grid Optimization Test Event.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("18a", "TH reads OptOutState attribute.", + "Verify value is 0x03 (OptOut)"), + TestStep("18b", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=InternalOptimization"), + TestStep("19", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, Cause=GridOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("20", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Test Event Clear.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("20a", "TH reads OptOutState attribute.", + "Verify value is 0x00 (NoOptOut)"), + TestStep("21", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, NominalPower=Forecast.Slots[0].NominalPower, MaximumEnergy=Forecast.Slots[0].NominalEnergy}, Cause=LocalOptimization.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("21a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=LocalOptimization"), + TestStep("22", "TH sends CancelRequest.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("22a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=InternalOptimization"), + TestStep("23", "TH sends CancelRequest.", + "Verify DUT responds with status INVALID_IN_STATE(0xcb)"), + TestStep("24", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Constraints-based Adjustment Adjustment Test Event Clear.", + "Verify DUT responds with status SUCCESS(0x00)"), + ] + + return steps + + @async_test_body + async def test_TC_DEM_2_7(self): + # pylint: disable=too-many-locals, too-many-statements + """Run the test steps.""" + self.step("1") + # Commission DUT - already done + + # Subscribe to Events and when they are sent push them to a queue for checking later + events_callback = EventChangeCallback(Clusters.DeviceEnergyManagement) + await events_callback.start(self.default_controller, + self.dut_node_id, + self.matter_test_config.endpoint) + + self.step("2") + await self.check_test_event_triggers_enabled() + + self.step("3") + await self.send_test_event_trigger_constraint_based_adjustment() + + self.step("3a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("3b") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + logging.info(forecast) + asserts.assert_greater(forecast.slots[0].nominalPower, 0) + asserts.assert_greater(forecast.slots[0].minPower, 0) + asserts.assert_greater(forecast.slots[0].maxPower, 0) + asserts.assert_greater(forecast.slots[0].nominalEnergy, 0) + + self.step("3c") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut) + + self.step("4") + # Matter UTC is time since 00:00:00 1/1/2000 + now = self.get_current_utc_time_in_seconds() + + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now - 10, duration=20, + nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("5") + now = self.get_current_utc_time_in_seconds() + + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 10, duration=20, + nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 20, duration=20, + nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 40, duration=20, + nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("6") + now = self.get_current_utc_time_in_seconds() + + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 10, duration=20, + nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 30, duration=20, + nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 40, duration=20, + nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("7") + now = self.get_current_utc_time_in_seconds() + + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 30, duration=20, + nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 10, duration=20, + nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 50, duration=20, + nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("8") + now = self.get_current_utc_time_in_seconds() + + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 10, duration=20, + nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 50, duration=20, + nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 30, duration=20, + nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("9") + absMaxPower = await self.read_dem_attribute_expect_success(attribute="AbsMaxPower") + + self.step("9a") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, nominalPower=absMaxPower + 1, maximumEnergy=forecast.slots[0].nominalEnergy)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("10") + absMinPower = await self.read_dem_attribute_expect_success(attribute="AbsMinPower") + + self.step("10a") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, nominalPower=absMinPower - 1, maximumEnergy=forecast.slots[0].nominalEnergy)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("11") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, nominalPower=forecast.slots[0].nominalPower)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.InvalidCommand) + + self.step("12") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, maximumEnergy=forecast.slots[0].nominalEnergy)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.InvalidCommand) + + self.step("13") + await self.send_test_event_trigger_user_opt_out_local() + + self.step("13a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("13b") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kLocalOptOut) + + self.step("14") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("15") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.Success) + + self.step("15a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kGridOptimization) + + self.step("16") + await self.send_cancel_request_command() + + self.step("16a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization) + + self.step("17") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.Success) + + self.step("17a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kGridOptimization) + + self.step("18") + await self.send_test_event_trigger_user_opt_out_grid() + + self.step("18a") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kOptOut) + + self.step("18b") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization) + + self.step("19") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.ConstraintError) + + self.step("20") + await self.send_test_event_trigger_user_opt_out_clear_all() + + self.step("20a") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut) + + self.step("21") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, nominalPower=forecast.slots[0].nominalPower, maximumEnergy=forecast.slots[0].nominalEnergy)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.Success) + + self.step("21a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kLocalOptimization) + + self.step("22") + await self.send_cancel_request_command() + + self.step("22a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization) + + self.step("23") + await self.send_cancel_request_command(expected_status=Status.InvalidInState) + + self.step("24") + await self.send_test_event_trigger_constraint_based_adjustment_clear() + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_DEM_2_8.py b/src/python_testing/TC_DEM_2_8.py new file mode 100644 index 00000000000000..773648c7e3ba8e --- /dev/null +++ b/src/python_testing/TC_DEM_2_8.py @@ -0,0 +1,307 @@ +# +# Copyright (c) 2024 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: disable=invalid-name + +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments +# for details about the block below. +# +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: run1 +# test-runner-run/run1/app: ${ENERGY_MANAGEMENT_APP} +# test-runner-run/run1/factoryreset: True +# test-runner-run/run1/quiet: True +# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json --enable-key 000102030405060708090a0b0c0d0e0f --featureSet 0x7c +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --hex-arg enableKey:000102030405060708090a0b0c0d0e0f --endpoint 1 --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto +# === END CI TEST ARGUMENTS === + +"""Define Matter test case TC_DEM_2_8.""" + + +import logging + +import chip.clusters as Clusters +from chip.interaction_model import Status +from matter_testing_support import EventChangeCallback, MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from mobly import asserts +from TC_DEMTestBase import DEMTestBase + +logger = logging.getLogger(__name__) + + +class TC_DEM_2_8(MatterBaseTest, DEMTestBase): + """Implementation of test case TC_DEM_2_8.""" + + def desc_TC_DEM_2_8(self) -> str: + """Return a description of this test.""" + return "4.1.3. [TC-DEM-2.8] Constraints-based Adjustment with State Forecast Reporting feature functionality with DUT as Server" + + def pics_TC_DEM_2_8(self): + """Return the PICS definitions associated with this test.""" + pics = [ + # Depends on Feature 06 (ConstraintBasedAdjustment) & Feature 02 (StateForecastReporting) + "DEM.S.F06", "DEM.S.F02" + ] + return pics + + def steps_TC_DEM_2_8(self) -> list[TestStep]: + """Execute the test steps.""" + steps = [ + TestStep("1", "Commissioning, already done", + is_commissioning=True), + TestStep("2", "TH reads TestEventTriggersEnabled attribute from General Diagnostics Cluster.", + "Verify value is 1 (True)"), + TestStep("3", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Constraints-based Adjustment Test Event.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("3a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("3b", "TH reads Forecast attribute.", + "Value has to include valid slots[0].ManufacturerESAState"), + TestStep("3c", "TH reads OptOutState attribute.", + "Verify value is 0x00 (NoOptOut)"), + TestStep("4", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=now()-10, Duration=20, LoadControl=0}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("5", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=now()+10, Duration=20, LoadControl=0}, constraints[1].{StartTime=now()+20, Duration=20, LoadControl=0}, constraints[2].{StartTime=now()+50, Duration=20, LoadControl=0}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("6", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=now()+10, Duration=20, LoadControl=0}, constraints[1].{StartTime=now()+30, Duration=20, LoadControl=0}, constraints[2].{StartTime=now()+40, Duration=20, LoadControl=0}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("7", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=now()+30, Duration=20, LoadControl=0}, constraints[1].{StartTime=now()+10, Duration=20, LoadControl=0}, constraints[2].{StartTime=now()+50, Duration=20, LoadControl=0}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("8", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=now()+10, Duration=20, LoadControl=0}, constraints[1].{StartTime=now()+50, Duration=20, LoadControl=0}, constraints[2].{StartTime=now()+30, Duration=20, LoadControl=0}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("9", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, LoadControl=101}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("10", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, LoadControl=-101}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("11", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Local Optimization Test Event.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("11a", "TH reads ESAState attribute.", + "Verify value is 0x01 (Online)"), + TestStep("11b", "TH reads OptOutState attribute.", + "Verify value is 0x02 (LocalOptOut)"), + TestStep("12", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, LoadControl=1}, Cause=LocalOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("13", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, LoadControl=1}, Cause=GridOptimization.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("13a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=GridOptimization"), + TestStep("14", "TH sends CancelRequest.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("14a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=InternalOptimization"), + TestStep("15", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, LoadControl=1}, Cause=GridOptimization.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("15a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=GridOptimization"), + TestStep("16", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Grid Optimization Test Event.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("16a", "TH reads OptOutState attribute.", + "Verify value is 0x03 (OptOut)"), + TestStep("16b", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=InternalOptimization"), + TestStep("17", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, LoadControl=1}, Cause=GridOptimization.", + "Verify DUT responds with status CONSTRAINT_ERROR(0x87)"), + TestStep("18", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for User Opt-out Test Event Clear.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("18a", "TH reads OptOutState attribute.", + "Verify value is 0x00 (NoOptOut)"), + TestStep("19", "TH sends RequestConstraintBasedPowerForecast with constraints[0].{StartTime=Forecast.StartTime, Duration=Forecast.Slots[0].DefaultDuration, LoadControl=1}, Cause=LocalOptimization.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("19a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=LocalOptimization"), + TestStep("20", "TH sends CancelRequest.", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("20a", "TH reads Forecast attribute.", + "Value has to include ForecastUpdateReason=InternalOptimization"), + TestStep("21", "TH sends CancelRequest.", + "Verify DUT responds with status INVALID_IN_STATE(0xcb)"), + TestStep("22", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Constraints-based Adjustment Adjustment Test Event Clear.", + "Verify DUT responds with status SUCCESS(0x00)"), + ] + + return steps + + @async_test_body + async def test_TC_DEM_2_8(self): + # pylint: disable=too-many-locals, too-many-statements + """Run the test steps.""" + self.step("1") + # Commission DUT - already done + + # Subscribe to Events and when they are sent push them to a queue for checking later + events_callback = EventChangeCallback(Clusters.DeviceEnergyManagement) + await events_callback.start(self.default_controller, + self.dut_node_id, + self.matter_test_config.endpoint) + + self.step("2") + await self.check_test_event_triggers_enabled() + + self.step("3") + await self.send_test_event_trigger_constraint_based_adjustment() + + self.step("3a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("3b") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_is_not_none(forecast.slots[0].manufacturerESAState) + + self.step("3c") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut) + + self.step("4") + # Matter UTC is time since 00:00:00 1/1/2000 + now = self.get_current_utc_time_in_seconds() + + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=now - 10, duration=20, loadControl=0)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("5") + # Matter UTC is time since 00:00:00 1/1/2000 + now = self.get_current_utc_time_in_seconds() + + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 10, duration=20, loadControl=0), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 20, duration=20, loadControl=0), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 50, duration=20, loadControl=0)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("6") + # Matter UTC is time since 00:00:00 1/1/2000 + now = self.get_current_utc_time_in_seconds() + + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 10, duration=20, loadControl=0), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 30, duration=20, loadControl=0), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 40, duration=20, loadControl=0)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("7") + now = self.get_current_utc_time_in_seconds() + + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 30, duration=20, loadControl=0), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 10, duration=20, loadControl=0), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 50, duration=20, loadControl=0)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("8") + now = self.get_current_utc_time_in_seconds() + + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 10, duration=20, loadControl=0), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 50, duration=20, loadControl=0), + Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct(startTime=now + 30, duration=20, loadControl=0)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("9") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, loadControl=101)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("10") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, loadControl=-101)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("11") + await self.send_test_event_trigger_user_opt_out_local() + + self.step("11a") + await self.check_dem_attribute("ESAState", Clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline) + + self.step("11b") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kLocalOptOut) + + self.step("12") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, loadControl=1)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.ConstraintError) + + self.step("13") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, loadControl=1)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.Success) + + self.step("13a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kGridOptimization) + + self.step("14") + await self.send_cancel_request_command() + + self.step("14a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization) + + self.step("15") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, loadControl=1)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.Success) + + self.step("15a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kGridOptimization) + + self.step("16") + await self.send_test_event_trigger_user_opt_out_grid() + + self.step("16a") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kOptOut) + + self.step("16b") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization) + + self.step("17") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, loadControl=1)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kGridOptimization, expected_status=Status.ConstraintError) + + self.step("18") + await self.send_test_event_trigger_user_opt_out_clear_all() + + self.step("18a") + await self.check_dem_attribute("OptOutState", Clusters.DeviceEnergyManagement.Enums.OptOutStateEnum.kNoOptOut) + + self.step("19") + constraintList = [Clusters.DeviceEnergyManagement.Structs.ConstraintsStruct( + startTime=forecast.startTime, duration=forecast.slots[0].defaultDuration, loadControl=1)] + await self.send_request_constraint_based_forecast(constraintList, cause=Clusters.DeviceEnergyManagement.Enums.AdjustmentCauseEnum.kLocalOptimization, expected_status=Status.Success) + + self.step("19a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kLocalOptimization) + + self.step("20") + await self.send_cancel_request_command() + + self.step("20a") + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_equal(forecast.forecastUpdateReason, + Clusters.DeviceEnergyManagement.Enums.ForecastUpdateReasonEnum.kInternalOptimization) + + self.step("21") + await self.send_cancel_request_command(expected_status=Status.InvalidInState) + + self.step("22") + await self.send_test_event_trigger_constraint_based_adjustment_clear() + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_DEM_2_9.py b/src/python_testing/TC_DEM_2_9.py new file mode 100644 index 00000000000000..8e60b69b481d93 --- /dev/null +++ b/src/python_testing/TC_DEM_2_9.py @@ -0,0 +1,120 @@ +# +# Copyright (c) 2024 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: disable=invalid-name + +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments +# for details about the block below. +# +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: run1 +# test-runner-run/run1/app: ${ENERGY_MANAGEMENT_APP} +# test-runner-run/run1/factoryreset: True +# test-runner-run/run1/quiet: True +# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json --enable-key 000102030405060708090a0b0c0d0e0f --featureSet 0x7e +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --hex-arg enableKey:000102030405060708090a0b0c0d0e0f --endpoint 1 --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto +# === END CI TEST ARGUMENTS === + +"""Define Matter test case TC_DEM_2_9.""" + + +import logging + +import chip.clusters as Clusters +from matter_testing_support import EventChangeCallback, MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from mobly import asserts +from TC_DEMTestBase import DEMTestBase + +logger = logging.getLogger(__name__) + + +class TC_DEM_2_9(MatterBaseTest, DEMTestBase): + """Implementation of test case TC_DEM_2_9.""" + + def desc_TC_DEM_2_9(self) -> str: + """Return a description of this test.""" + return "4.1.3. [TC-DEM-2.2] Power or State Forecast Reporting feature functionality with DUT as Server" + + def pics_TC_DEM_2_9(self): + """Return the PICS definitions associated with this test.""" + pics = [ + # Depends on Feature 01 (PowerForecastReporting) | Feature 2 (StateForecastReporting) + "DEM.S.F01", "DEM.S.F02", + ] + return pics + + def steps_TC_DEM_2_9(self) -> list[TestStep]: + """Execute the test steps.""" + steps = [ + TestStep("1", "Commissioning, already done", + is_commissioning=True), + TestStep("2", "TH reads TestEventTriggersEnabled attribute from General Diagnostics Cluster.", + "Verify that TestEventTriggersEnabled attribute has a value of 1 (True)"), + TestStep("3", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Forecast Test Event", + "Verify DUT responds with status SUCCESS(0x00)"), + TestStep("3a", "TH reads Forecast attribute.", + "Value has to include a valid slots[0].ManufacturerESAState"), + TestStep("3b", "TH reads Forecast attribute.", + "Value has to include valid slots[0].NominalPower, slots[0].MinPower, slots[0].MaxPower, slots[0].NominalEnergy"), + TestStep("4", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.DEM.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.DEM.TEST_EVENT_TRIGGER for Forecast Test Event Clear", + "Verify DUT responds with status SUCCESS(0x00)"), + ] + + return steps + + @async_test_body + async def test_TC_DEM_2_9(self): + # pylint: disable=too-many-locals, too-many-statements + """Run the test steps.""" + self.step("1") + # Commission DUT - already done + + # Subscribe to Events and when they are sent push them to a queue for checking later + events_callback = EventChangeCallback(Clusters.DeviceEnergyManagement) + await events_callback.start(self.default_controller, + self.dut_node_id, + self.matter_test_config.endpoint) + + self.step("2") + await self.check_test_event_triggers_enabled() + + self.step("3") + await self.send_test_event_trigger_forecast() + + self.step("3a") + feature_map = await self.read_dem_attribute_expect_success(attribute="FeatureMap") + if feature_map & Clusters.DeviceEnergyManagement.Bitmaps.Feature.kStateForecastReporting: + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + asserts.assert_is_not_none(forecast.slots[0].manufacturerESAState) + else: + logging.info('Device does not support StateForecastReporting. Skipping step 3a') + + self.step("3b") + if feature_map & Clusters.DeviceEnergyManagement.Bitmaps.Feature.kPowerForecastReporting: + forecast = await self.read_dem_attribute_expect_success(attribute="Forecast") + + asserts.assert_is_not_none(forecast.slots[0].nominalPower) + asserts.assert_is_not_none(forecast.slots[0].minPower) + asserts.assert_is_not_none(forecast.slots[0].maxPower) + asserts.assert_is_not_none(forecast.slots[0].nominalEnergy) + else: + logging.info('Device does not support StateForecastReporting. Skipping step 3b') + + self.step("4") + await self.send_test_event_trigger_forecast_clear() + + +if __name__ == "__main__": + default_matter_test_main() From 07c85dcfbc397f007f4b1e39acac7f4c875ac41a Mon Sep 17 00:00:00 2001 From: cdj <45139296+DejinChen@users.noreply.github.com> Date: Fri, 26 Jul 2024 18:35:03 +0800 Subject: [PATCH 2/3] ESP32: added config for Wi-Fi, Thread, and Ethernet network drivers. (#34052) * ESP32: added config for Wi-Fi, Thread, and Ethernet network commissioning drivers * Update README.md * Restyled * Restyled by prettier-markdown * Fix CI --------- Co-authored-by: Restyled.io --- config/esp32/components/chip/CMakeLists.txt | 3 ++ config/esp32/components/chip/Kconfig | 54 +++++++++++++++++++ examples/all-clusters-app/esp32/README.md | 18 ++++--- .../esp32/sdkconfig.defaults | 3 ++ .../platform/esp32/common/Esp32AppServer.cpp | 18 +------ examples/shell/esp32/sdkconfig.defaults | 3 ++ src/platform/ESP32/CHIPDevicePlatformConfig.h | 15 ++++++ .../ESP32/ConnectivityManagerImpl.cpp | 34 ++++++++++++ 8 files changed, 125 insertions(+), 23 deletions(-) diff --git a/config/esp32/components/chip/CMakeLists.txt b/config/esp32/components/chip/CMakeLists.txt index f27b29baf91ff9..6ddfc4839123ff 100644 --- a/config/esp32/components/chip/CMakeLists.txt +++ b/config/esp32/components/chip/CMakeLists.txt @@ -224,6 +224,9 @@ endif() if (CONFIG_ENABLE_MATTER_OVER_THREAD) chip_gn_arg_append("chip_enable_openthread" "true") + if (CONFIG_THREAD_NETWORK_COMMISSIONING_DRIVER) + chip_gn_arg_append("chip_device_config_thread_network_endpoint_id" ${CONFIG_THREAD_NETWORK_ENDPOINT_ID}) + endif() else() chip_gn_arg_append("chip_enable_openthread" "false") endif() diff --git a/config/esp32/components/chip/Kconfig b/config/esp32/components/chip/Kconfig index c742ddb0336764..5cd2700bb972b6 100644 --- a/config/esp32/components/chip/Kconfig +++ b/config/esp32/components/chip/Kconfig @@ -157,6 +157,12 @@ menu "CHIP Core" help Option to enable/disable CHIP log level filtering APIs. + config ENABLE_CHIP_DATA_MODEL + bool "Enable CHIP data model" + default y + help + Option to enable/disable CHIP data model. + endmenu # "General Options" menu "Networking Options" @@ -1289,4 +1295,52 @@ menu "CHIP Device Layer" endmenu + menu "Network Commissioning Driver Endpoint Id" + config THREAD_NETWORK_COMMISSIONING_DRIVER + bool "Use the generic Thread network commissioning driver" + depends on ENABLE_MATTER_OVER_THREAD && ENABLE_CHIP_DATA_MODEL + default y + help + Option to enable/disable the use of generic Thread network commissioning driver. + + config THREAD_NETWORK_ENDPOINT_ID + int "Endpoint Id for Thread network" + depends on THREAD_NETWORK_COMMISSIONING_DRIVER + range 0 65534 + default 0 + help + The endpoint id for the generic Thread network commissioning driver. + + config WIFI_NETWORK_COMMISSIONING_DRIVER + bool "Use ESP Wi-Fi network commissioning driver" + depends on (ENABLE_WIFI_STATION || ENABLE_WIFI_AP) && ENABLE_CHIP_DATA_MODEL + default y + help + Option to enable/disable the use of ESP Wi-Fi network commissioning driver. + + config WIFI_NETWORK_ENDPOINT_ID + int "Endpoint Id for Wi-Fi network" + depends on WIFI_NETWORK_COMMISSIONING_DRIVER + range 0 65534 + default 0 + help + The endpoint id for the ESP Wi-Fi network commissioning driver. + + config ETHERNET_NETWORK_COMMISSIONING_DRIVER + bool "Use ESP Ethernet network commissioning driver" + depends on ENABLE_ETHERNET_TELEMETRY && ENABLE_CHIP_DATA_MODEL + default y + help + Option to enable/disable the use of ESP Ethernet network commissioning driver. + + config ETHERNET_NETWORK_ENDPOINT_ID + int "Endpoint Id for Ethernet network" + depends on ETHERNET_NETWORK_COMMISSIONING_DRIVER + range 0 65534 + default 0 + help + The endpoint id for the ESP Ethernet network commissioning driver. + + endmenu + endmenu diff --git a/examples/all-clusters-app/esp32/README.md b/examples/all-clusters-app/esp32/README.md index 60ee180c1337fd..0a3145dbc24da2 100644 --- a/examples/all-clusters-app/esp32/README.md +++ b/examples/all-clusters-app/esp32/README.md @@ -47,14 +47,20 @@ NetworkCommissioning Endpoint can be used to manage the driver of extra network interface. For ESP32-C6 DevKits, if `CHIP_DEVICE_CONFIG_ENABLE_WIFI` and -`CHIP_DEVICE_CONFIG_ENABLE_THREAD` are both enabled, the NetworkCommissioning -cluster in Endpoint 0 will be used for Thread network driver and the same -cluster on Endpoint 65534 will be used for Wi-Fi network driver. +`CHIP_DEVICE_CONFIG_ENABLE_THREAD` are both enabled, please set +`CONFIG_THREAD_NETWORK_ENDPOINT_ID` to 0 and set +`CONFIG_WIFI_NETWORK_ENDPOINT_ID` to 65534, which presents that the +NetworkCommissioning cluster in Endpoint 0 will be used for Thread network +driver and the same cluster on Endpoint 65534 will be used for Wi-Fi network +driver. Or vice versa. For ESP32-Ethernet-Kits, if `CHIP_DEVICE_CONFIG_ENABLE_WIFI` and -`CHIP_DEVICE_CONFIG_ENABLE_ETHERNET` are both enabled, the NetworkCommissioning -cluster in Endpoint 0 will be used for Ethernet network driver and the same -cluster on Endpoint 65534 will be used for Wi-Fi network driver. +`CHIP_DEVICE_CONFIG_ENABLE_ETHERNET` are both enabled, please set +`CONFIG_ETHERNET_NETWORK_ENDPOINT_ID` to 0 and set +`CONFIG_WIFI_NETWORK_ENDPOINT_ID` to 65534, which presents that the +NetworkCommissioning cluster in Endpoint 0 will be used for Ethernet network +driver and the same cluster on Endpoint 65534 will be used for Wi-Fi network +driver. Or vice versa. --- diff --git a/examples/persistent-storage/esp32/sdkconfig.defaults b/examples/persistent-storage/esp32/sdkconfig.defaults index f9a4c4bbcb05da..45488e615dece7 100644 --- a/examples/persistent-storage/esp32/sdkconfig.defaults +++ b/examples/persistent-storage/esp32/sdkconfig.defaults @@ -32,3 +32,6 @@ CONFIG_DEVICE_PRODUCT_ID=0x8009 # Enable HKDF in mbedtls CONFIG_MBEDTLS_HKDF_C=y + +# Disable CHIP data model +CONFIG_ENABLE_CHIP_DATA_MODEL=n diff --git a/examples/platform/esp32/common/Esp32AppServer.cpp b/examples/platform/esp32/common/Esp32AppServer.cpp index 1c30098e9010d5..8ee5d4d7a05a27 100644 --- a/examples/platform/esp32/common/Esp32AppServer.cpp +++ b/examples/platform/esp32/common/Esp32AppServer.cpp @@ -51,19 +51,6 @@ using namespace chip::DeviceLayer; static constexpr char TAG[] = "ESP32Appserver"; namespace { -#if CHIP_DEVICE_CONFIG_ENABLE_WIFI -#if CHIP_DEVICE_CONFIG_ENABLE_THREAD || CHIP_DEVICE_CONFIG_ENABLE_ETHERNET -constexpr chip::EndpointId kNetworkCommissioningEndpointWiFi = 0xFFFE; -#else -constexpr chip::EndpointId kNetworkCommissioningEndpointWiFi = 0; -#endif -app::Clusters::NetworkCommissioning::Instance - sWiFiNetworkCommissioningInstance(kNetworkCommissioningEndpointWiFi, &(NetworkCommissioning::ESPWiFiDriver::GetInstance())); -#endif // CHIP_DEVICE_CONFIG_ENABLE_WIFI -#if CHIP_DEVICE_CONFIG_ENABLE_ETHERNET -static app::Clusters::NetworkCommissioning::Instance - sEthernetNetworkCommissioningInstance(0 /* Endpoint Id */, &(NetworkCommissioning::ESPEthernetDriver::GetInstance())); -#endif #if CONFIG_TEST_EVENT_TRIGGER_ENABLED static uint8_t sTestEventTriggerEnableKey[TestEventTriggerDelegate::kEnableKeyLength] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, @@ -150,14 +137,11 @@ void Esp32AppServer::Init(AppDelegate * sAppDelegate) chip::Server::GetInstance().Init(initParams); #if CHIP_DEVICE_CONFIG_ENABLE_WIFI - sWiFiNetworkCommissioningInstance.Init(); #ifdef CONFIG_ENABLE_CHIP_SHELL chip::Shell::SetWiFiDriver(&(chip::DeviceLayer::NetworkCommissioning::ESPWiFiDriver::GetInstance())); #endif #endif -#if CHIP_DEVICE_CONFIG_ENABLE_ETHERNET - sEthernetNetworkCommissioningInstance.Init(); -#endif + #if CHIP_DEVICE_CONFIG_ENABLE_THREAD if (chip::DeviceLayer::ConnectivityMgr().IsThreadProvisioned() && (chip::Server::GetInstance().GetFabricTable().FabricCount() != 0)) diff --git a/examples/shell/esp32/sdkconfig.defaults b/examples/shell/esp32/sdkconfig.defaults index a77fbf41167c0c..efd5d235a1cf54 100644 --- a/examples/shell/esp32/sdkconfig.defaults +++ b/examples/shell/esp32/sdkconfig.defaults @@ -41,3 +41,6 @@ CONFIG_ENABLE_CHIP_SHELL=y # Enable HKDF in mbedtls CONFIG_MBEDTLS_HKDF_C=y + +# Disable CHIP data model +CONFIG_ENABLE_CHIP_DATA_MODEL=n diff --git a/src/platform/ESP32/CHIPDevicePlatformConfig.h b/src/platform/ESP32/CHIPDevicePlatformConfig.h index e347df109d1e3a..dc4f380c262fb5 100644 --- a/src/platform/ESP32/CHIPDevicePlatformConfig.h +++ b/src/platform/ESP32/CHIPDevicePlatformConfig.h @@ -50,6 +50,9 @@ #ifdef CONFIG_ENABLE_MATTER_OVER_THREAD #define CHIP_DEVICE_CONFIG_ENABLE_THREAD CONFIG_ENABLE_MATTER_OVER_THREAD +#ifndef CONFIG_THREAD_NETWORK_COMMISSIONING_DRIVER +#define _NO_NETWORK_COMMISSIONING_DRIVER_ 1 +#endif #else #define CHIP_DEVICE_CONFIG_ENABLE_THREAD 0 #endif // CONFIG_ENABLE_MATTER_OVER_THREAD @@ -164,3 +167,15 @@ #define CHIP_DEVICE_CONFIG_BG_TASK_PRIORITY CONFIG_BG_CHIP_TASK_PRIORITY #define CHIP_DEVICE_CONFIG_BG_MAX_EVENT_QUEUE_SIZE CONFIG_BG_MAX_EVENT_QUEUE_SIZE #define CHIP_DEVICE_CONFIG_BG_TASK_STACK_SIZE CONFIG_BG_CHIP_TASK_STACK_SIZE + +#ifdef CONFIG_WIFI_NETWORK_COMMISSIONING_DRIVER +#define CHIP_DEVICE_CONFIG_WIFI_NETWORK_DRIVER CONFIG_WIFI_NETWORK_COMMISSIONING_DRIVER +#else +#define CHIP_DEVICE_CONFIG_WIFI_NETWORK_DRIVER 0 +#endif // CONFIG_WIFI_NETWORK_COMMISSIONING_DRIVER + +#ifdef CONFIG_ETHERNET_NETWORK_COMMISSIONING_DRIVER +#define CHIP_DEVICE_CONFIG_ETHERNET_NETWORK_DRIVER CONFIG_ETHERNET_NETWORK_COMMISSIONING_DRIVER +#else +#define CHIP_DEVICE_CONFIG_ETHERNET_NETWORK_DRIVER 0 +#endif // CONFIG_ETHERNET_NETWORK_COMMISSIONING_DRIVER diff --git a/src/platform/ESP32/ConnectivityManagerImpl.cpp b/src/platform/ESP32/ConnectivityManagerImpl.cpp index 85146257ebe985..2b042a5d5c2fcb 100644 --- a/src/platform/ESP32/ConnectivityManagerImpl.cpp +++ b/src/platform/ESP32/ConnectivityManagerImpl.cpp @@ -39,9 +39,11 @@ #include #endif +#include #include #include #include +#include #include using namespace ::chip; @@ -53,6 +55,18 @@ namespace chip { namespace DeviceLayer { ConnectivityManagerImpl ConnectivityManagerImpl::sInstance; + +#if CHIP_DEVICE_CONFIG_ENABLE_WIFI && CHIP_DEVICE_CONFIG_WIFI_NETWORK_DRIVER +app::Clusters::NetworkCommissioning::Instance + sWiFiNetworkCommissioningInstance(CONFIG_WIFI_NETWORK_ENDPOINT_ID /* Endpoint Id */, + &(NetworkCommissioning::ESPWiFiDriver::GetInstance())); +#endif + +#if CHIP_DEVICE_CONFIG_ENABLE_ETHERNET && CHIP_DEVICE_CONFIG_ETHERNET_NETWORK_DRIVER +app::Clusters::NetworkCommissioning::Instance + sEthernetNetworkCommissioningInstance(CONFIG_ETHERNET_NETWORK_ENDPOINT_ID /* Endpoint Id */, + &(NetworkCommissioning::ESPEthernetDriver::GetInstance())); +#endif // ==================== ConnectivityManager Platform Internal Methods ==================== CHIP_ERROR ConnectivityManagerImpl::_Init() @@ -62,10 +76,30 @@ CHIP_ERROR ConnectivityManagerImpl::_Init() #endif #if CHIP_DEVICE_CONFIG_ENABLE_WIFI InitWiFi(); +#if CHIP_DEVICE_CONFIG_WIFI_NETWORK_DRIVER + sWiFiNetworkCommissioningInstance.Init(); +#endif // CHIP_DEVICE_CONFIG_WIFI_NETWORK_DRIVER #endif #if CHIP_DEVICE_CONFIG_ENABLE_ETHERNET InitEthernet(); +#if CHIP_DEVICE_CONFIG_ETHERNET_NETWORK_DRIVER + sEthernetNetworkCommissioningInstance.Init(); +#endif // CHIP_DEVICE_CONFIG_ETHERNET_NETWORK_DRIVER #endif + +#if defined(CONFIG_WIFI_NETWORK_ENDPOINT_ID) && defined(CONFIG_THREAD_NETWORK_ENDPOINT_ID) + static_assert(CONFIG_WIFI_NETWORK_ENDPOINT_ID != CONFIG_THREAD_NETWORK_ENDPOINT_ID, + "Wi-Fi network endpoint id and Thread network endpoint id should not be the same."); +#endif +#if defined(CONFIG_WIFI_NETWORK_ENDPOINT_ID) && defined(CONFIG_ETHERNET_NETWORK_ENDPOINT_ID) + static_assert(CONFIG_WIFI_NETWORK_ENDPOINT_ID != CONFIG_ETHERNET_NETWORK_ENDPOINT_ID, + "Wi-Fi network endpoint id and Ethernet network endpoint id should not be the same."); +#endif +#if defined(CONFIG_THREAD_NETWORK_ENDPOINT_ID) && defined(CONFIG_ETHERNET_NETWORK_ENDPOINT_ID) + static_assert(CONFIG_THREAD_NETWORK_ENDPOINT_ID != CONFIG_ETHERNET_NETWORK_ENDPOINT_ID, + "Thread network endpoint id and Ethernet network endpoint id should not be the same."); +#endif + return CHIP_NO_ERROR; } From 9ef5bbfd520d8847970e59b343ae50a655463749 Mon Sep 17 00:00:00 2001 From: jamesharrow <93921463+jamesharrow@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:24:38 +0100 Subject: [PATCH 3/3] EVSE add get set clear targets support (#31407) * De-alphabetize list of files to avoid breaking GH action * Semi-realphabetize? * Restore strangely dropped events * Better BitMask handling * Change min/max on electrical measurements to be decimal instead of hex * Rename meas-and-sense to measurement-and-sensing.xml * Remove seemingly superfluous attribute requirements on Descriptor cluster on Electrical Measurement * Updates to electrical-power-measurement-server based on comments * Remove defaults from MeasurementAccuracyRangeStruct to match spec update * Restore side="server" to events * Move common enums and bitmaps to detail:: instead of detail::Enums and detail::Bitmaps; remove superfluous using statement * Assign ID to Electrical Sensor device type * Removed EPM and EEM from Root Node Device * Restyled formatting is different than clang-format * Re-add FeatureMap to attributeAccessInterfaceAttributes for EEM and EPM * Regen after merge * Added electrical-energy-measurement-server to CMakelist to fix linker issue. * Lock client on Electrical Sensor device type * Remove unneeded using statement now that Enums are in detail:: * Check for null iterators and error * Switch to ResourceExhausted from CHIP_ERROR_INTERNAL * Re-enabled EEM in energy management app and regen all after previous merge * Some refactoring to add EPM Instance into the EVSEManufacturer class to clean up containment. Added ability to fake voltage, power and current to the TE triggers. * Missed one file. * Fixed crash due to unassigned dg pointer. Power/Voltage/Current faking working too. * Touch file since restyled crashed * Restyled by gn * Restyled by isort * Add stub for EPM cluster * Reverted whitespace change * Did regen_all after merge from master to resolve conflicts. * Put back line of clusters which somehow got deleted accidentally. * Remerged ZAP file and regen all after resolving conflicts. * Fixes for Python tests * Correct name for Ember init callback * Formatting * Sync optional attributes list with .zap file for EPM * Add missing features to EPM stub * Revert FeatureMap in attributeAccessInterfaceAttributes * Allow FeatureMap in EEM constructor; add all-clusters-app EEM stub * Forgot zcl-with-test-extensions * Unregister EEM attribute access in destructor * Remove redundant returns to keep clang-tidy happy * Fix for issue mentioned in code review on EEM cluster limiting the number of endpoints it allows. * Refactoring to have a common EvseMain across all platforms to avoid making changes in multiple places * Added electrical-power-measurement-server to ESP32 CMakeLists.txt * Updated Matter device types to add EVSE * Open and saved energy-management-app.zap and regen_all * Removed duplicate ElectricalEnergyMeasurment class which was accidentally merged in. Fixed issue raised about ElectricalEnergyMeasurement array size not working on bridges. * Added support for test event triggers and handling of reading events into matter_testing_support. * Made TC_EEVSE_Utils.py use the matter_testing_support instead of its own local copy. * Restyled by isort * Cherry pick from Tweaks to EVSE Test plans (Issue #31460) * Changed the random value generation to make the values +/- and handle sign conversion to avoid compile warnings * Enabled cumulativeEnergyExported in Energy-management-app. * Added initial electrical power measurement 2.2 test case * Changed copyright date * Code review comment fixes. * Changed to c++ style cast * Fixed trailing whitepace * Added support for testing read of EEM attributes and change of values * Corrected EPM references in TC_EEM_2_2. Added TC_EEM_2_3 * Added periodic energy reporting, and new cumulativeEnergyReset attribute into energy-management-app.zap * Added periodic energy reading support and TC_EEM_2_3 to 2_5. * Python removed unused logging and EventChangeCallback * Updates to align to test plan PR #3949 * Added initial EEM_2_1 test script. * Added example of setting EEM Accuracy and EEM CumulativeEnergyReset structure - TC_EEM_2_1 now passes * Restyled by whitespace * Restyled * Removed extra spaces in TC_EEM_2_1.py * Removed unused EnergyManagementManager.cpp/.h * Fixed PowerMode = kAc * Initial TC_EPM_2_1.py script * Restyled by isort * Merged TC_EEVSE tests back in * Initialized NumberOfMeasurementTypes * Added EEM 2.1,2.2,2.3,2.4,2.5 and EPM 2.1,2.2 into CI workflow tests.yaml * Interim state - partially refactored how Measurement Structs are encoded similar to how ModeBase clusters are implemented. Needs tidy up. Will break all-clusters for now * Removed SetNumberOfMeasurementTypes since this can be derived from the ArraySize(kMeasurementAccuracies). Added more stringent checking in test script of measurementTypes and ranges. * Completed TC_EPM_2_1.py script * Corrected test plan spec reference. * Test EPM_2_1 now runs and passes. Allows checking that attributes are supported, and skips test if not. Validation of values ignores Nulls (which are allowed). Turned on Ranges attribute. * Revert unintended change to tests.yaml * Python test case code-review updates * Removed old range iterator. * Fixed lint issues and adjusted timings to match the test plan pr. * Fixed all-clusters electrical-power-measurement cluster by using the energy-management-app/common Delegate * Implemented HarmonicCurrents and HarmonicStructs (to return empty list for now) * Updated TC_EEVSE_2_3.py from more up to date branch. * Added basic set/get/clear targets framework based on earlier work. Not working * Removed files that were recently deleted upstream * Corrected PICS in TC_EEVSE_2_3.py and copyright date * Interim checkin with GetTargets not working * Initial GetTargets working with a constant static array to demonstrate the command response can be encoded ok * Removed unused EnergyEvseManager.cpp, Added EnergyEvseTargetStore.cpp * Added EVSETargets to DefaultStorageKeyAllocator.h * Reverted incorrect removal of EnergyEvseManager.cpp * Added override into EnergyEvseDelegate which resulted in strange errors on some platforms. * Added basic infrastructure for storing targets and reading them back. Compiles and runs * Store working * Store and Load seem to work. * Fixed Status change caused by upstream changes * Added helper function to compute day of week bitmap * Fix to evse-stub in all-clusters to add gEvseTargetsDelegate * Fixed build error on some platforms: cast of dayOfWeek to uint8_t * Refactoring of GetTargets command to build the response using dynamic memory * Updated evse-server.cpp/.h * Tidy up of unused CommonIterator in electrical-power-measurement-server.h * Get Targets reading back from file working * Starting to clean up - ran through Valgrind to check for memory leaks. * More clean up * Added Clear Targets support and initial implementation of SetTargets * Fixed crash when erasing entries - test reported PASS! * Attempt to fix Darwin complaint about unsigned int cast * Added logging of get targets response * Fixed platform specific logging compilation issue * Clean up of unused code * More clean up * Removed unused variable - Darwin check * clang checker updates * Refactored code to fix missing added energy since the TargetSoC could be optional and calling Next() would skip AddedEnergy * Added checking of GetTargetsResponse - PASSES * Almost working but need to resolve the Charging current and start time calculation in some scenarios. * Fixed PEP8 lint errors in TC_EVSE_2_3.py and TC_EEVSE_Utils.py taking advantage of new TestStep 3rd arg * Restyled by isort * Fixed ChipLogDetail %d errors on some platforms with a static_cast * Fixes in EVSE FindTargets to remove signed/unsigned comparison. Calculation of charging time was out by factor of 10. * Fixed TC_EEVSE_2_3.py to match test plan. Fixed EVSE FindNextTarget to handle targets if searching for future days. Now passes tests. * Fixed FindNextTarget to use epoch_s for NextChargeTargetTime and NextChargeStartTime with associated TC_EEVSE_2_3.py changes. Fixed bug when TargetSoC = 100% caused start time to be reported incorrectly. * Fix: When EVSE is not plugged in, or not enabled for charge we shouldn't compute a schedule. Also add callbacks to update schedule if the state changes. * Updated TC_EEM, TC_EPM, TC_EEVSE to take advantage of the new TestStep 3rd element. * Address comments from Boris Zbarsky * Fix lint error by adding entry for src/app/clusters/energy-evse-server/energy-evse-server.h * Revert "Fix lint error by adding entry for src/app/clusters/energy-evse-server/energy-evse-server.h" This reverts commit 7a60876809ce0af89a9edb00909122a27f922a5e. * Rework HandleGetTargets following https://github.com/project-chip/connectedhomeip/pull/33736 * Address comments from Boris Zbarsky * Update TC_EEVSE_2_3 to align with latest test spec * Remove the use of vector * Get all targets building again * Catch up from master + fix some lint and build errors * Restyled by whitespace * Restyled by clang-format * Restyled by autopep8 * Restyled by isort * Fixing regession-build error * Restyled by clang-format * Fixing regession-build error * Changed logic to not return NextChargeRequiredEnergy if not plugged in or not enabled for charging. * Removed max 80A current limit which aligns to the spec. Who knows how many Amps chargers will have in the future! * Bug fix - When disabling the mMaximumChargingCurrentLimitFromCommand was not being updated, so we didn't get attribute updates for MaximumChargeCurrent when toggling Enable-Disable-Enable. * Being into line with latest EVSE test spec although TODOs around the time of use trigger * Load the targets in EnergyEvseInit() * Made EvseTargetsEntryType -> evseTargetsEntryType (since it is a variable not a type) * Reformatting and correcting the cluster PICS to be EEVSE not DEM. * Reverted changes to python test cases which don't belong in this PR - See PR#34243 (makes this PR 10 files smaller). * Address review comments from James Harrow * Address review comments from James Harrow * Add energy unit tests * Fix test case to align with PR10027 * Move the EVSE targets key to be a private member of EnergyEvseTargetsStore * Move EvseTargetStore test harness to examples/energy-management-app/energy-management-common/tests * Move example tests to examples/BUILD.gn * Restyled by clang-format * Restyled by gn * Add a mechanism to enable the targets persistent data to be delete when the last fabric is deleted * Fix unused variable compilation issues * Restyled by whitespace * Restyled by clang-format * Adde TC_EEVSE_2_3.py to CI testing. * Added new test-runner info to top of Python TC_EEVSE_2_3.py * Remove assert from BUILD.gn so test harness can build * Put BEGIN CI TEST ARGUMENTS around runner test arguments * Fix compilation problem in unit test * Try and fix the zephyr build problems * Restyled by gn * Try and fix the zephyr build problems * Try and fix the zephyr build problems * Try and fix the zephyr build problems * Restyled by gn * Start addressing comments from Andrei * Continuing addressing comments from Andrei * Continuing addressing comments from Andrei * Restyled by clang-format * Fix a problem with the return code spotted by Boris * Update examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp Co-authored-by: Andrei Litvin * Update examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp Co-authored-by: Andrei Litvin * Update examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp Co-authored-by: Andrei Litvin * Update with further review comments from Andrei * Update with further review comments from Andrei * Update with further review comments from Andrei * Get all targets building * Restyled by whitespace * Restyled by clang-format * Update with further review comments from Andrei * Address further review comments from Andrei * Restyled by clang-format * Complete rename of Reset to ChargingTargetsMemMgr::PrepareDaySchedule * Fix potential memory leak * Add check in ChargingTargetsMemMgr::PrepareDaySchedule against a bad chargingTargetSchedulesIdx * Restyled by clang-format * Address final comments from Andrei * Restyled by gn * Removed whitespace in openiotsdk/CMakelists.txt * Fix merge issue * Fix CI build issue --------- Co-authored-by: Hasty Granbery Co-authored-by: Restyled.io Co-authored-by: pcoleman Co-authored-by: PeterC1965 <101805108+PeterC1965@users.noreply.github.com> Co-authored-by: Andrei Litvin --- .github/workflows/tests.yaml | 1 + BUILD.gn | 2 + examples/BUILD.gn | 32 ++ .../src/energy-evse-stub.cpp | 12 +- .../all-clusters-app/ameba/chip_main.cmake | 4 +- examples/all-clusters-app/asr/BUILD.gn | 2 + .../all-clusters-app/cc13x2x7_26x2x7/BUILD.gn | 0 .../all-clusters-app/cc13x4_26x4/BUILD.gn | 2 + .../all-clusters-app/infineon/psoc6/BUILD.gn | 2 + examples/all-clusters-app/linux/BUILD.gn | 2 + examples/all-clusters-app/mbed/CMakeLists.txt | 4 +- .../nrfconnect/CMakeLists.txt | 4 +- examples/all-clusters-app/nxp/mw320/BUILD.gn | 2 + .../openiotsdk/CMakeLists.txt | 4 +- .../all-clusters-app/telink/CMakeLists.txt | 4 +- examples/all-clusters-app/tizen/BUILD.gn | 2 + .../include/ChargingTargetsMemMgr.h | 156 ++++++ .../include/EVSEManufacturerImpl.h | 6 + .../include/EnergyEvseDelegateImpl.h | 72 ++- .../include/EnergyEvseTargetsStore.h | 123 +++++ .../src/ChargingTargetsMemMgr.cpp | 193 +++++++ .../src/EVSEManufacturerImpl.cpp | 223 +++++++- .../src/EnergyEvseDelegateImpl.cpp | 277 +++++++--- .../src/EnergyEvseEventTriggers.cpp | 17 +- .../src/EnergyEvseMain.cpp | 27 +- .../src/EnergyEvseManager.cpp | 11 + .../src/EnergyEvseTargetsStore.cpp | 489 ++++++++++++++++++ .../energy-management-common/tests/BUILD.gn | 44 ++ .../tests/TestEvseTargetsStorage.cpp | 219 ++++++++ examples/energy-management-app/linux/BUILD.gn | 21 +- examples/energy-management-app/linux/main.cpp | 6 +- examples/platform/linux/BUILD.gn | 8 +- examples/shell/shell_common/BUILD.gn | 2 + src/BUILD.gn | 9 + .../electrical-energy-measurement-server.cpp | 1 + .../electrical-power-measurement-server.h | 3 - .../EnergyEvseTestEventTriggerHandler.h | 4 + .../energy-evse-server/energy-evse-server.cpp | 137 ++++- .../energy-evse-server/energy-evse-server.h | 37 +- src/python_testing/TC_EEVSE_2_3.py | 454 ++++++++++++++++ src/python_testing/TC_EEVSE_Utils.py | 71 ++- 41 files changed, 2568 insertions(+), 121 deletions(-) create mode 100644 examples/BUILD.gn create mode 100644 examples/all-clusters-app/cc13x2x7_26x2x7/BUILD.gn create mode 100644 examples/energy-management-app/energy-management-common/include/ChargingTargetsMemMgr.h create mode 100644 examples/energy-management-app/energy-management-common/include/EnergyEvseTargetsStore.h create mode 100644 examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp create mode 100644 examples/energy-management-app/energy-management-common/src/EnergyEvseTargetsStore.cpp create mode 100644 examples/energy-management-app/energy-management-common/tests/BUILD.gn create mode 100644 examples/energy-management-app/energy-management-common/tests/TestEvseTargetsStorage.cpp create mode 100644 src/python_testing/TC_EEVSE_2_3.py diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d17e5db8a2a541..51832814ac548d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -527,6 +527,7 @@ jobs: scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_EEM_2_4.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_EEM_2_5.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_EEVSE_2_2.py' + scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_EEVSE_2_3.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_EEVSE_2_4.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_EEVSE_2_5.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_EPM_2_1.py' diff --git a/BUILD.gn b/BUILD.gn index ddd893a4ca49d8..ecf710efdf8a15 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -146,6 +146,8 @@ if (current_toolchain != "${dir_pw_toolchain}/default:default") { if (chip_build_tests) { deps += [ "//src:tests" ] + deps += [ "//examples:example_tests" ] + if (current_os == "android" && current_toolchain == default_toolchain) { deps += [ "${chip_root}/build/chip/java/tests:java_build_test" ] } diff --git a/examples/BUILD.gn b/examples/BUILD.gn new file mode 100644 index 00000000000000..a8cb18422949e2 --- /dev/null +++ b/examples/BUILD.gn @@ -0,0 +1,32 @@ +# Copyright (c) 2020-2021 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import("//build_overrides/build.gni") +import("//build_overrides/chip.gni") + +import("${build_root}/config/compiler/compiler.gni") +import("${chip_root}/build/chip/tests.gni") +import("${chip_root}/src/platform/device.gni") + +if (chip_build_tests) { + import("${chip_root}/build/chip/chip_test_group.gni") + + chip_test_group("example_tests") { + deps = [] + tests = [] + if (chip_device_platform == "linux" && current_os == "linux") { + tests += [ "${chip_root}/examples/energy-management-app/energy-management-common/tests" ] + } + } +} diff --git a/examples/all-clusters-app/all-clusters-common/src/energy-evse-stub.cpp b/examples/all-clusters-app/all-clusters-common/src/energy-evse-stub.cpp index 72fe588995784e..6acc81ac285b65 100644 --- a/examples/all-clusters-app/all-clusters-common/src/energy-evse-stub.cpp +++ b/examples/all-clusters-app/all-clusters-common/src/energy-evse-stub.cpp @@ -23,14 +23,24 @@ using namespace chip::app::Clusters; using namespace chip::app::Clusters::EnergyEvse; static std::unique_ptr gDelegate; +static std::unique_ptr gEvseTargetsDelegate; static std::unique_ptr gInstance; void emberAfEnergyEvseClusterInitCallback(chip::EndpointId endpointId) { VerifyOrDie(endpointId == 1); // this cluster is only enabled for endpoint 1. + VerifyOrDie(!gDelegate); + VerifyOrDie(!gEvseTargetsDelegate); VerifyOrDie(!gInstance); - gDelegate = std::make_unique(); + gEvseTargetsDelegate = std::make_unique(); + if (!gEvseTargetsDelegate) + { + ChipLogError(AppServer, "Failed to allocate memory for EvseTargetsDelegate"); + return; + } + + gDelegate = std::make_unique(*gEvseTargetsDelegate); if (gDelegate) { gInstance = std::make_unique( diff --git a/examples/all-clusters-app/ameba/chip_main.cmake b/examples/all-clusters-app/ameba/chip_main.cmake index ce557a2bffaf30..e6e9ee19a87394 100644 --- a/examples/all-clusters-app/ameba/chip_main.cmake +++ b/examples/all-clusters-app/ameba/chip_main.cmake @@ -187,13 +187,15 @@ list( ${chip_dir}/examples/microwave-oven-app/microwave-oven-common/src/microwave-oven-device.cpp - ${chip_dir}/examples/energy-management-app/energy-management-common/src/EnergyTimeUtils.cpp + ${chip_dir}/examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp ${chip_dir}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementDelegateImpl.cpp ${chip_dir}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementManager.cpp ${chip_dir}/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp ${chip_dir}/examples/energy-management-app/energy-management-common/src/ElectricalPowerMeasurementDelegate.cpp ${chip_dir}/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp ${chip_dir}/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp + ${chip_dir}/examples/energy-management-app/energy-management-common/src/EnergyEvseTargetsStore.cpp + ${chip_dir}/examples/energy-management-app/energy-management-common/src/EnergyTimeUtils.cpp ${chip_dir}/examples/platform/ameba/route_hook/ameba_route_hook.c ${chip_dir}/examples/platform/ameba/route_hook/ameba_route_table.c diff --git a/examples/all-clusters-app/asr/BUILD.gn b/examples/all-clusters-app/asr/BUILD.gn index 6c9d334a1ed6c2..fc7ad9037b99de 100644 --- a/examples/all-clusters-app/asr/BUILD.gn +++ b/examples/all-clusters-app/asr/BUILD.gn @@ -82,12 +82,14 @@ asr_executable("clusters_app") { "${chip_root}/examples/all-clusters-app/all-clusters-common/src/smco-stub.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/static-supported-modes-manager.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/static-supported-temperature-levels.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementManager.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/ElectricalPowerMeasurementDelegate.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseTargetsStore.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyTimeUtils.cpp", "${examples_plat_dir}/ButtonHandler.cpp", "${examples_plat_dir}/CHIPDeviceManager.cpp", diff --git a/examples/all-clusters-app/cc13x2x7_26x2x7/BUILD.gn b/examples/all-clusters-app/cc13x2x7_26x2x7/BUILD.gn new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/examples/all-clusters-app/cc13x4_26x4/BUILD.gn b/examples/all-clusters-app/cc13x4_26x4/BUILD.gn index 45efe2d45e9bce..c1cb81a9722f7d 100644 --- a/examples/all-clusters-app/cc13x4_26x4/BUILD.gn +++ b/examples/all-clusters-app/cc13x4_26x4/BUILD.gn @@ -72,12 +72,14 @@ ti_simplelink_executable("all-clusters-app") { "${chip_root}/examples/all-clusters-app/all-clusters-common/src/smco-stub.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/static-supported-modes-manager.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/static-supported-temperature-levels.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementManager.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/ElectricalPowerMeasurementDelegate.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseTargetsStore.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyTimeUtils.cpp", "${chip_root}/examples/providers/DeviceInfoProviderImpl.cpp", "${chip_root}/src/app/clusters/general-diagnostics-server/GenericFaultTestEventTriggerHandler.cpp", diff --git a/examples/all-clusters-app/infineon/psoc6/BUILD.gn b/examples/all-clusters-app/infineon/psoc6/BUILD.gn index 2c0b0a6fee7dfa..9be06891342d7c 100644 --- a/examples/all-clusters-app/infineon/psoc6/BUILD.gn +++ b/examples/all-clusters-app/infineon/psoc6/BUILD.gn @@ -124,12 +124,14 @@ psoc6_executable("clusters_app") { "${chip_root}/examples/all-clusters-app/all-clusters-common/src/smco-stub.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/static-supported-modes-manager.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/static-supported-temperature-levels.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementManager.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/ElectricalPowerMeasurementDelegate.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseTargetsStore.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyTimeUtils.cpp", "${examples_plat_dir}/LEDWidget.cpp", "${examples_plat_dir}/init_psoc6Platform.cpp", diff --git a/examples/all-clusters-app/linux/BUILD.gn b/examples/all-clusters-app/linux/BUILD.gn index ca1149b25e7891..3564153a482cca 100644 --- a/examples/all-clusters-app/linux/BUILD.gn +++ b/examples/all-clusters-app/linux/BUILD.gn @@ -59,12 +59,14 @@ source_set("chip-all-clusters-common") { "${chip_root}/examples/all-clusters-app/all-clusters-common/src/tcc-mode.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/water-heater-mode.cpp", "${chip_root}/examples/all-clusters-app/linux/diagnostic-logs-provider-delegate-impl.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementManager.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/ElectricalPowerMeasurementDelegate.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseTargetsStore.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyTimeUtils.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/device-energy-management-mode.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/energy-evse-mode.cpp", diff --git a/examples/all-clusters-app/mbed/CMakeLists.txt b/examples/all-clusters-app/mbed/CMakeLists.txt index 9b490ff687d1e3..427517aff9f8e1 100644 --- a/examples/all-clusters-app/mbed/CMakeLists.txt +++ b/examples/all-clusters-app/mbed/CMakeLists.txt @@ -72,13 +72,15 @@ target_sources(${APP_TARGET} PRIVATE ${ALL_CLUSTERS_COMMON}/src/smco-stub.cpp ${ALL_CLUSTERS_COMMON}/src/static-supported-modes-manager.cpp ${ALL_CLUSTERS_COMMON}/src/static-supported-temperature-levels.cpp - ${ENERGY_MANAGEMENT_COMMON}/src/EnergyTimeUtils.cpp + ${ENERGY_MANAGEMENT_COMMON}/src/ChargingTargetsMemMgr.cpp ${ENERGY_MANAGEMENT_COMMON}/src/DeviceEnergyManagementDelegateImpl.cpp ${ENERGY_MANAGEMENT_COMMON}/src/DeviceEnergyManagementManager.cpp ${ENERGY_MANAGEMENT_COMMON}/src/EVSEManufacturerImpl.cpp ${ENERGY_MANAGEMENT_COMMON}/src/ElectricalPowerMeasurementDelegate.cpp ${ENERGY_MANAGEMENT_COMMON}/src/EnergyEvseDelegateImpl.cpp ${ENERGY_MANAGEMENT_COMMON}/src/EnergyEvseManager.cpp + ${ENERGY_MANAGEMENT_COMMON}/src/EnergyEvseTargetsStore.cpp + ${ENERGY_MANAGEMENT_COMMON}/src/EnergyTimeUtils.cpp ) chip_configure_data_model(${APP_TARGET} diff --git a/examples/all-clusters-app/nrfconnect/CMakeLists.txt b/examples/all-clusters-app/nrfconnect/CMakeLists.txt index c290003532a431..e11dcbd14356fc 100644 --- a/examples/all-clusters-app/nrfconnect/CMakeLists.txt +++ b/examples/all-clusters-app/nrfconnect/CMakeLists.txt @@ -62,13 +62,15 @@ target_sources(app PRIVATE ${ALL_CLUSTERS_COMMON_DIR}/src/air-quality-instance.cpp ${ALL_CLUSTERS_COMMON_DIR}/src/concentration-measurement-instances.cpp ${ALL_CLUSTERS_COMMON_DIR}/src/resource-monitoring-delegates.cpp - ${ENERGY_MANAGEMENT_COMMON_DIR}/src/EnergyTimeUtils.cpp + ${ENERGY_MANAGEMENT_COMMON_DIR}/src/ChargingTargetsMemMgr.cpp ${ENERGY_MANAGEMENT_COMMON_DIR}/src/DeviceEnergyManagementDelegateImpl.cpp ${ENERGY_MANAGEMENT_COMMON_DIR}/src/DeviceEnergyManagementManager.cpp ${ENERGY_MANAGEMENT_COMMON_DIR}/src/EVSEManufacturerImpl.cpp ${ENERGY_MANAGEMENT_COMMON_DIR}/src/ElectricalPowerMeasurementDelegate.cpp ${ENERGY_MANAGEMENT_COMMON_DIR}/src/EnergyEvseDelegateImpl.cpp ${ENERGY_MANAGEMENT_COMMON_DIR}/src/EnergyEvseManager.cpp + ${ENERGY_MANAGEMENT_COMMON_DIR}/src/EnergyEvseTargetsStore.cpp + ${ENERGY_MANAGEMENT_COMMON_DIR}/src/EnergyTimeUtils.cpp ${NRFCONNECT_COMMON}/util/LEDWidget.cpp) chip_configure_data_model(app diff --git a/examples/all-clusters-app/nxp/mw320/BUILD.gn b/examples/all-clusters-app/nxp/mw320/BUILD.gn index 83877a175993df..e5e3671143ee99 100644 --- a/examples/all-clusters-app/nxp/mw320/BUILD.gn +++ b/examples/all-clusters-app/nxp/mw320/BUILD.gn @@ -89,12 +89,14 @@ mw320_executable("shell_mw320") { "${chip_root}/examples/all-clusters-app/all-clusters-common/src/smco-stub.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/static-supported-modes-manager.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/static-supported-temperature-levels.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementManager.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/ElectricalPowerMeasurementDelegate.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseTargetsStore.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyTimeUtils.cpp", "${chip_root}/src/lib/shell/streamer_mw320.cpp", "binding-handler.cpp", diff --git a/examples/all-clusters-app/openiotsdk/CMakeLists.txt b/examples/all-clusters-app/openiotsdk/CMakeLists.txt index 1395cf170a8b79..9db62a3df783a1 100644 --- a/examples/all-clusters-app/openiotsdk/CMakeLists.txt +++ b/examples/all-clusters-app/openiotsdk/CMakeLists.txt @@ -65,13 +65,15 @@ target_sources(${APP_TARGET} ${ALL_CLUSTERS_COMMON}/src/resource-monitoring-delegates.cpp ${ALL_CLUSTERS_COMMON}/src/static-supported-modes-manager.cpp ${ALL_CLUSTERS_COMMON}/src/binding-handler.cpp - ${ENERGY_MANAGEMENT_COMMON}/src/EnergyTimeUtils.cpp + ${ENERGY_MANAGEMENT_COMMON}/src/ChargingTargetsMemMgr.cpp ${ENERGY_MANAGEMENT_COMMON}/src/DeviceEnergyManagementDelegateImpl.cpp ${ENERGY_MANAGEMENT_COMMON}/src/DeviceEnergyManagementManager.cpp ${ENERGY_MANAGEMENT_COMMON}/src/EVSEManufacturerImpl.cpp ${ENERGY_MANAGEMENT_COMMON}/src/ElectricalPowerMeasurementDelegate.cpp ${ENERGY_MANAGEMENT_COMMON}/src/EnergyEvseDelegateImpl.cpp ${ENERGY_MANAGEMENT_COMMON}/src/EnergyEvseManager.cpp + ${ENERGY_MANAGEMENT_COMMON}/src/EnergyEvseTargetsStore.cpp + ${ENERGY_MANAGEMENT_COMMON}/src/EnergyTimeUtils.cpp ) target_link_libraries(${APP_TARGET} diff --git a/examples/all-clusters-app/telink/CMakeLists.txt b/examples/all-clusters-app/telink/CMakeLists.txt index 149b38e3528cce..675ea4d9eb7e74 100644 --- a/examples/all-clusters-app/telink/CMakeLists.txt +++ b/examples/all-clusters-app/telink/CMakeLists.txt @@ -51,13 +51,15 @@ target_sources(app PRIVATE ${ALL_CLUSTERS_COMMON_DIR}/src/device-energy-management-stub.cpp ${ALL_CLUSTERS_COMMON_DIR}/src/energy-evse-stub.cpp ${ALL_CLUSTERS_COMMON_DIR}/src/resource-monitoring-delegates.cpp - ${ENERGY_MANAGEMENT_COMMON_DIR}/src/EnergyTimeUtils.cpp + ${ENERGY_MANAGEMENT_COMMON_DIR}/src/ChargingTargetsMemMgr.cpp ${ENERGY_MANAGEMENT_COMMON_DIR}/src/DeviceEnergyManagementDelegateImpl.cpp ${ENERGY_MANAGEMENT_COMMON_DIR}/src/DeviceEnergyManagementManager.cpp ${ENERGY_MANAGEMENT_COMMON_DIR}/src/EVSEManufacturerImpl.cpp ${ENERGY_MANAGEMENT_COMMON_DIR}/src/ElectricalPowerMeasurementDelegate.cpp ${ENERGY_MANAGEMENT_COMMON_DIR}/src/EnergyEvseDelegateImpl.cpp ${ENERGY_MANAGEMENT_COMMON_DIR}/src/EnergyEvseManager.cpp + ${ENERGY_MANAGEMENT_COMMON_DIR}/src/EnergyEvseTargetsStore.cpp + ${ENERGY_MANAGEMENT_COMMON_DIR}/src/EnergyTimeUtils.cpp ${TELINK_COMMON}/common/src/mainCommon.cpp ${TELINK_COMMON}/common/src/AppTaskCommon.cpp ${TELINK_COMMON}/util/src/LEDManager.cpp diff --git a/examples/all-clusters-app/tizen/BUILD.gn b/examples/all-clusters-app/tizen/BUILD.gn index 9aff82b57b2876..855416c0446923 100644 --- a/examples/all-clusters-app/tizen/BUILD.gn +++ b/examples/all-clusters-app/tizen/BUILD.gn @@ -38,12 +38,14 @@ source_set("chip-all-clusters-common") { "${chip_root}/examples/all-clusters-app/all-clusters-common/src/smco-stub.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/static-supported-modes-manager.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/static-supported-temperature-levels.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementManager.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/ElectricalPowerMeasurementDelegate.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseTargetsStore.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyTimeUtils.cpp", ] diff --git a/examples/energy-management-app/energy-management-common/include/ChargingTargetsMemMgr.h b/examples/energy-management-app/energy-management-common/include/ChargingTargetsMemMgr.h new file mode 100644 index 00000000000000..66009cdd84ca0e --- /dev/null +++ b/examples/energy-management-app/energy-management-common/include/ChargingTargetsMemMgr.h @@ -0,0 +1,156 @@ +/* + * + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +namespace chip { +namespace app { +namespace Clusters { +namespace EnergyEvse { + +/* + * The full Target data structure defined as: + * + * DataModel::List chargingTargetSchedules; + * + * contains a list of ChargingTargetScheduleStructs which in turn contains a list of ChargingTargetStructs. + * This means that somewhere the following memory needs to be allocated in the case where + * the Target is at its maximum size: + * + * ChargingTargetStruct::Type mDailyChargingTargets[kEvseTargetsMaxNumberOfDays][kEvseTargetsMaxTargetsPerDay] + * + * This is 1680B. + * + * However it is likely the number of chargingTargets configured will be considerably less. To avoid + * allocating the maximum possible Target size, each List is allocated + * separately. This class handles that allocation. + * + * When iterating through the chargingTargetSchedules, an index in this list is kept and the + * ChargingTargetsMemMgr::PrepareDaySchedule must be called so this object knows which day schedule it is tracking. + * This will free any previous memory allocated for the day schedule in this object. + * + * There are then three usage cases: + * + * 1. When loading the Target from persistent storage. In this scenario, it is not known upfront + * how many chargingTargets are associated with this day schedule so ChargingTargetsMemMgr::AddChargingTarget() + * needs to be called as each individual chargingTarget is loaded from persistent data. + * + * Once the chargingTargets for the day schedule have been loaded, ChargingTargetsMemMgr::AllocAndCopy() is + * called to allocate the memory to store the chargingTargets and the chargingTargets are copied. + * + * 2. When updating a Target and a day schedule is unaffected, the chargingTargets associated with + * day schedule need copying. The following should be called: + * + * ChargingTargetsMemMgr::AllocAndCopy(const DataModel::List & chargingTargets) + * + * 3. When in SetTargets, a new list of chargingTargets needs to be added to a day schedule, the following + * should be called: + * + * ChargingTargetsMemMgr::AllocAndCopy(const DataModel::DecodableList & + * chargingTargets) + * + * Having allocated and copied the chargingTargets accordingly, they can be added to a + * DataModel::List(ChargingTargetsMemMgr::GetChargingTargets(), + * ChargingTargetsMemMgr::GetNumDailyChargingTargets()); + * + * All memory allocated by this object is released When the ChargingTargetsMemMgr destructor is called. + * + */ + +class ChargingTargetsMemMgr +{ +public: + ChargingTargetsMemMgr(); + ~ChargingTargetsMemMgr(); + + /** + * @brief This method prepares a new day schedule. Subsequent calls to GetChargingTargets + * and the AllocAndCopy methods below will reference this day schedule. + * + * @param chargingTargetSchedulesIdx - The new day schedule index + */ + void PrepareDaySchedule(uint16_t chargingTargetSchedulesIdx); + + /** + * @brief Called as each individual chargingTarget is loaded from persistent data. + * When loading the Target from persistent storage, it is not known upfront + * how many chargingTargets are associated with this day schedule so + * ChargingTargetsMemMgr::AddChargingTarget() needs to be called as each individual + * chargingTarget is loaded from persistent data. + * + * @param chargingTarget - The chargingTarget that will be added into the current day schedule + */ + void AddChargingTarget(const EnergyEvse::Structs::ChargingTargetStruct::Type & chargingTarget); + + /** + * @brief Called to allocate and copy the chargingTargets added via AddChargingTarget into the + * current day schedule as set by PrepareDaySchedule(). + */ + CHIP_ERROR AllocAndCopy(); + + /** + * @brief Called to allocate and copy the chargingTargets into the current day schedule as set + * set by PrepareDaySchedule(). + * If an attempt is made to add more than kEvseTargetsMaxTargetsPerDay chargingTargets + * for the current day schedule, then the chargingTarget is not added and an error message + * is printed. + * + * @param chargingTargets - The chargingTargets to add into the current day schedule + */ + CHIP_ERROR AllocAndCopy(const DataModel::List & chargingTargets); + + /** + * @brief Called to allocate and copy the chargingTargets into the current day schedule as set + * set by PrepareDaySchedule(). + * + * @param chargingTargets - The chargingTargets to add into the current day schedule + */ + CHIP_ERROR AllocAndCopy(const DataModel::DecodableList & chargingTargets); + + /** + * @brief Returns the list of chargingTargets associated with the current day schedule. + * + * @return The charging targets associated with the current day schedule. + */ + EnergyEvse::Structs::ChargingTargetStruct::Type * GetChargingTargets() const; + + /** + * @brief Returns the number of chargingTargets associated with current day schedule. + * + * @return Returns the number of chargingTargets associated with current day schedule. + */ + uint16_t GetNumDailyChargingTargets() const; + +private: + EnergyEvse::Structs::ChargingTargetStruct::Type * mpListOfDays[kEvseTargetsMaxNumberOfDays]; + EnergyEvse::Structs::ChargingTargetStruct::Type mDailyChargingTargets[kEvseTargetsMaxTargetsPerDay]; + uint16_t mChargingTargetSchedulesIdx; + uint16_t mNumDailyChargingTargets; +}; + +} // namespace EnergyEvse +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/examples/energy-management-app/energy-management-common/include/EVSEManufacturerImpl.h b/examples/energy-management-app/energy-management-common/include/EVSEManufacturerImpl.h index 2faf81046cfdf2..b94220d11b28f3 100644 --- a/examples/energy-management-app/energy-management-common/include/EVSEManufacturerImpl.h +++ b/examples/energy-management-app/energy-management-common/include/EVSEManufacturerImpl.h @@ -129,6 +129,12 @@ class EVSEManufacturer : public DEMManufacturerDelegate */ static void ApplicationCallbackHandler(const EVSECbInfo * cb, intptr_t arg); + /** + * @brief Simple example to demonstrate how an EVSE can compute the start time + * and duration of a charging schedule + */ + CHIP_ERROR ComputeChargingSchedule(); + /** * @brief Allows a client application to initialise the Accuracy, Measurement types etc */ diff --git a/examples/energy-management-app/energy-management-common/include/EnergyEvseDelegateImpl.h b/examples/energy-management-app/energy-management-common/include/EnergyEvseDelegateImpl.h index aed42fa857968a..c43707d9d09589 100644 --- a/examples/energy-management-app/energy-management-common/include/EnergyEvseDelegateImpl.h +++ b/examples/energy-management-app/energy-management-common/include/EnergyEvseDelegateImpl.h @@ -20,6 +20,7 @@ #include "app/clusters/energy-evse-server/energy-evse-server.h" #include +#include #include #include @@ -31,6 +32,10 @@ namespace app { namespace Clusters { namespace EnergyEvse { +// A bitmap of all possible days (the union of the values in +// chip::app::Clusters::EnergyEvse::TargetDayOfWeekBitmap) +constexpr uint8_t kAllTargetDaysMask = 0x7f; + /* Local state machine Events to allow simpler handling of state transitions */ enum EVSEStateMachineEvent { @@ -100,8 +105,11 @@ class EvseSession class EnergyEvseDelegate : public EnergyEvse::Delegate { public: + EnergyEvseDelegate(EvseTargetsDelegate & aDelegate) : EnergyEvse::Delegate() { mEvseTargetsDelegate = &aDelegate; } ~EnergyEvseDelegate(); + EvseTargetsDelegate * GetEvseTargetsDelegate() { return mEvseTargetsDelegate; } + /** * @brief Called when EVSE cluster receives Disable command */ @@ -131,6 +139,37 @@ class EnergyEvseDelegate : public EnergyEvse::Delegate */ Status StartDiagnostics() override; + /** + * @brief Called when EVSE cluster receives the SetTargets command + */ + Status SetTargets( + const DataModel::DecodableList & chargingTargetSchedules) override; + + /** + * @brief Delegate should implement a handler for LoadTargets + * + * This needs to load any stored targets into memory and MUST be called before + * GetTargets is called. + */ + Status LoadTargets() override; + + /** + * @brief Called when EVSE cluster receives the GetTargets command + * + * NOTE: LoadTargets MUST be called GetTargets is called. + */ + Protocols::InteractionModel::Status + GetTargets(DataModel::List & chargingTargetSchedules) override; + + /** + * @brief Called when EVSE cluster receives ClearTargets command + */ + Status ClearTargets() override; + + /* Helper functions for managing targets*/ + Status + ValidateTargets(const DataModel::DecodableList & chargingTargetSchedules); + /** * @brief Called by EVSE Hardware to register a single callback handler */ @@ -153,6 +192,12 @@ class EnergyEvseDelegate : public EnergyEvse::Delegate */ Status ScheduleCheckOnEnabledTimeout(); + /** + * @brief Helper function to get know if the EV is plugged in based on state + * (regardless of if it is actually transferring energy) + */ + bool IsEvsePluggedIn(); + // ----------------------------------------------------------------- // Internal API to allow an EVSE to change its internal state etc Status HwSetMaxHardwareCurrentLimit(int64_t currentmA); @@ -209,9 +254,16 @@ class EnergyEvseDelegate : public EnergyEvse::Delegate /* PREF attributes */ DataModel::Nullable GetNextChargeStartTime() override; + CHIP_ERROR SetNextChargeStartTime(DataModel::Nullable newNextChargeStartTimeUtc); + DataModel::Nullable GetNextChargeTargetTime() override; + CHIP_ERROR SetNextChargeTargetTime(DataModel::Nullable newNextChargeTargetTimeUtc); + DataModel::Nullable GetNextChargeRequiredEnergy() override; + CHIP_ERROR SetNextChargeRequiredEnergy(DataModel::Nullable newNextChargeRequiredEnergyMilliWattH); + DataModel::Nullable GetNextChargeTargetSoC() override; + CHIP_ERROR SetNextChargeTargetSoC(DataModel::Nullable newValue); DataModel::Nullable GetApproximateEVEfficiency() override; CHIP_ERROR SetApproximateEVEfficiency(DataModel::Nullable) override; @@ -229,11 +281,11 @@ class EnergyEvseDelegate : public EnergyEvse::Delegate private: /* Constants */ - static constexpr int kDefaultMinChargeCurrent = 6000; /* 6A */ - static constexpr int kDefaultUserMaximumChargeCurrent = kMaximumChargeCurrent; /* 80A */ - static constexpr int kDefaultRandomizationDelayWindow = 600; /* 600s */ - static constexpr int kMaxVehicleIDBufSize = 32; - static constexpr int kPeriodicCheckIntervalRealTimeClockNotSynced = 30; + static constexpr int kDefaultMinChargeCurrent_mA = 6000; /* 6A */ + static constexpr int kDefaultUserMaximumChargeCurrent_mA = 80000; /* 80A */ + static constexpr int kDefaultRandomizationDelayWindow_sec = 600; /* 600s */ + static constexpr int kMaxVehicleIDBufSize = 32; + static constexpr int kPeriodicCheckIntervalRealTimeClockNotSynced_sec = 30; /* private variables for controlling the hardware - these are not attributes */ int64_t mMaxHardwareCurrentLimit = 0; /* Hardware current limit in mA */ @@ -250,6 +302,7 @@ class EnergyEvseDelegate : public EnergyEvse::Delegate EVSECallbackWrapper mCallbacks = { .handler = nullptr, .arg = 0 }; /* Wrapper to allow callbacks to be registered */ Status NotifyApplicationCurrentLimitChange(int64_t maximumChargeCurrent); Status NotifyApplicationStateChange(); + Status NotifyApplicationChargingPreferencesChange(); Status GetEVSEEnergyMeterValue(ChargingDischargingType meterType, int64_t & aMeterValue); /* Local State machine handling */ @@ -285,11 +338,11 @@ class EnergyEvseDelegate : public EnergyEvse::Delegate DataModel::Nullable mChargingEnabledUntil; // TODO Default to 0 to indicate disabled DataModel::Nullable mDischargingEnabledUntil; // TODO Default to 0 to indicate disabled int64_t mCircuitCapacity = 0; - int64_t mMinimumChargeCurrent = kDefaultMinChargeCurrent; + int64_t mMinimumChargeCurrent = kDefaultMinChargeCurrent_mA; int64_t mMaximumChargeCurrent = 0; int64_t mMaximumDischargeCurrent = 0; - int64_t mUserMaximumChargeCurrent = kDefaultUserMaximumChargeCurrent; // TODO update spec - uint32_t mRandomizationDelayWindow = kDefaultRandomizationDelayWindow; + int64_t mUserMaximumChargeCurrent = kDefaultUserMaximumChargeCurrent_mA; // TODO update spec + uint32_t mRandomizationDelayWindow = kDefaultRandomizationDelayWindow_sec; /* PREF attributes */ DataModel::Nullable mNextChargeStartTime; DataModel::Nullable mNextChargeTargetTime; @@ -309,6 +362,9 @@ class EnergyEvseDelegate : public EnergyEvse::Delegate /* Helper variable to hold meter val since last EnergyTransferStarted event */ int64_t mMeterValueAtEnergyTransferStart; + + /* Targets Delegate */ + EvseTargetsDelegate * mEvseTargetsDelegate = nullptr; }; } // namespace EnergyEvse diff --git a/examples/energy-management-app/energy-management-common/include/EnergyEvseTargetsStore.h b/examples/energy-management-app/energy-management-common/include/EnergyEvseTargetsStore.h new file mode 100644 index 00000000000000..97379f8470e0ae --- /dev/null +++ b/examples/energy-management-app/energy-management-common/include/EnergyEvseTargetsStore.h @@ -0,0 +1,123 @@ +/* + * + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace chip { +namespace app { +namespace Clusters { +namespace EnergyEvse { + +class EvseTargetsDelegate : public chip::FabricTable::Delegate +{ +public: + EvseTargetsDelegate(); + ~EvseTargetsDelegate(); + + CHIP_ERROR Init(PersistentStorageDelegate * targetStore); + + /** + * @brief Delegate should implement a handler for LoadTargets + * + * This needs to load any stored targets into memory + */ + CHIP_ERROR LoadTargets(); + + /** + * @brief This returns a reference to the existing targets + */ + const DataModel::List & GetTargets(); + + /** + * @brief Copies a ChargingTargetSchedule into our store + * + * @param [in] an entry from the SetTargets list containing: + * dayOfWeekForSequence and chargingTargets (list) + * + * This routine scans the existing targets to see if we have a day of week + * set that matches the new target dayOfWeek bits. If there is an existing + * matching day then it replaces the days existing targets with the new entry + */ + CHIP_ERROR SetTargets( + const DataModel::DecodableList & chargingTargetSchedulesChanges); + + /** + * @brief This deletes all targets and resets the list to empty + */ + CHIP_ERROR ClearTargets(); + + /** + * Part of the FabricTable::Delegate interface. Gets called when a fabric is deleted, such as on FabricTable::Delete(). + **/ + virtual void OnFabricRemoved(const FabricTable & fabricTable, FabricIndex fabricIndex) override; + +private: + // This is the upper bound in bytes of the TLV storage required to store the chargingTargetSchedulesList + static uint16_t GetTlvSizeUpperBound(); + + CHIP_ERROR SaveTargets(DataModel::List & chargingTargetSchedulesList); + + // For debug purposes + void PrintTargets(const DataModel::List & chargingTargetSchedules); + +protected: + enum class TargetEntryTag : uint8_t + { + kTargetEntry = 1, + kDayOfWeek = 2, + kChargingTargetsList = 3, + kChargingTargetsStruct = 4, + kTargetTime = 5, + kTargetSoC = 6, + kAddedEnergy = 7, + }; + +private: + // Object to handle the allocation of memory for the chargingTargets + ChargingTargetsMemMgr mChargingTargets; + + // Need memory to store the ChargingTargetScheduleStruct as this is pointed to from a + // List + Structs::ChargingTargetScheduleStruct::Type mChargingTargetSchedulesArray[kEvseTargetsMaxNumberOfDays]; + + // The current Target definition + DataModel::List mChargingTargetSchedulesList; + + // Pointer to the PeristentStorage + PersistentStorageDelegate * mpTargetStore = nullptr; + + // Need a key to store the Charging Preference Targets which is a TLV of list of lists + static constexpr const char * spEvseTargetsKeyName = "g/ev/targ"; +}; + +} // namespace EnergyEvse +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp b/examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp new file mode 100644 index 00000000000000..1642b76882178b --- /dev/null +++ b/examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp @@ -0,0 +1,193 @@ +/* + * + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "ChargingTargetsMemMgr.h" + +using namespace chip; +using namespace chip::app; +using namespace chip::app::DataModel; +using namespace chip::app::Clusters; +using namespace chip::app::Clusters::EnergyEvse; + +ChargingTargetsMemMgr::ChargingTargetsMemMgr() : mChargingTargetSchedulesIdx(0), mNumDailyChargingTargets(0) +{ + memset(mpListOfDays, 0, sizeof(mpListOfDays)); +} + +ChargingTargetsMemMgr::~ChargingTargetsMemMgr() +{ + // Free all memory allocated for the charging targets + for (uint16_t idx = 0; idx < kEvseTargetsMaxNumberOfDays; idx++) + { + if (mpListOfDays[idx] != nullptr) + { + chip::Platform::Delete(mpListOfDays[idx]); + } + } +} + +void ChargingTargetsMemMgr::PrepareDaySchedule(uint16_t chargingTargetSchedulesIdx) +{ + // MUST be called for each entry in DataModel::List chargingTargetSchedules + mNumDailyChargingTargets = 0; + + // Should not occur but just to be safe + if (chargingTargetSchedulesIdx >= kEvseTargetsMaxNumberOfDays) + { + ChipLogError(AppServer, "PrepareDaySchedule bad chargingTargetSchedulesIdx %u", chargingTargetSchedulesIdx); + return; + } + + mChargingTargetSchedulesIdx = chargingTargetSchedulesIdx; + + // Free up any memory associated with this targetSchedule + if (mpListOfDays[mChargingTargetSchedulesIdx] != nullptr) + { + chip::Platform::MemoryFree(mpListOfDays[mChargingTargetSchedulesIdx]); + mpListOfDays[mChargingTargetSchedulesIdx] = nullptr; + } +} + +void ChargingTargetsMemMgr::AddChargingTarget(const EnergyEvse::Structs::ChargingTargetStruct::Type & chargingTarget) +{ + if (mNumDailyChargingTargets < kEvseTargetsMaxTargetsPerDay) + { + mDailyChargingTargets[mNumDailyChargingTargets++] = chargingTarget; + } + else + { + ChipLogError(AppServer, "AddChargingTarget: trying to add too many chargingTargets"); + } +} + +EnergyEvse::Structs::ChargingTargetStruct::Type * ChargingTargetsMemMgr::GetChargingTargets() const +{ + return mpListOfDays[mChargingTargetSchedulesIdx]; +} + +uint16_t ChargingTargetsMemMgr::GetNumDailyChargingTargets() const +{ + return mNumDailyChargingTargets; +} + +CHIP_ERROR ChargingTargetsMemMgr::AllocAndCopy() +{ + // NOTE: ChargingTargetsMemMgr::PrepareDaySchedule() must be called as specified in the class comments in + // ChargingTargetsMemMgr.h before this method can be called. + + VerifyOrDie(mpListOfDays[mChargingTargetSchedulesIdx] == nullptr); + + if (mNumDailyChargingTargets > 0) + { + // Allocate the memory first and then use placement new to initialise the memory of each element in the array + mpListOfDays[mChargingTargetSchedulesIdx] = static_cast( + chip::Platform::MemoryAlloc(sizeof(EnergyEvse::Structs::ChargingTargetStruct::Type) * mNumDailyChargingTargets)); + + VerifyOrReturnError(mpListOfDays[mChargingTargetSchedulesIdx] != nullptr, CHIP_ERROR_NO_MEMORY); + + for (uint16_t idx = 0; idx < mNumDailyChargingTargets; idx++) + { + // This will cause the ChargingTargetStruct constructor to be called and this element in the array + new (mpListOfDays[mChargingTargetSchedulesIdx] + idx) EnergyEvse::Structs::ChargingTargetStruct::Type(); + + // Now copy the chargingTarget + mpListOfDays[mChargingTargetSchedulesIdx][idx] = mDailyChargingTargets[idx]; + } + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR +ChargingTargetsMemMgr::AllocAndCopy(const DataModel::List & chargingTargets) +{ + // NOTE: ChargingTargetsMemMgr::PrepareDaySchedule() must be called as specified in the class comments in + // ChargingTargetsMemMgr.h before this method can be called. + + VerifyOrDie(mpListOfDays[mChargingTargetSchedulesIdx] == nullptr); + + mNumDailyChargingTargets = static_cast(chargingTargets.size()); + + if (mNumDailyChargingTargets > 0) + { + // Allocate the memory first and then use placement new to initialise the memory of each element in the array + mpListOfDays[mChargingTargetSchedulesIdx] = static_cast( + chip::Platform::MemoryAlloc(sizeof(EnergyEvse::Structs::ChargingTargetStruct::Type) * chargingTargets.size())); + + VerifyOrReturnError(mpListOfDays[mChargingTargetSchedulesIdx] != nullptr, CHIP_ERROR_NO_MEMORY); + + uint16_t idx = 0; + for (auto & chargingTarget : chargingTargets) + { + // This will cause the ChargingTargetStruct constructor to be called and this element in the array + new (mpListOfDays[mChargingTargetSchedulesIdx] + idx) EnergyEvse::Structs::ChargingTargetStruct::Type(); + + // Now copy the chargingTarget + mpListOfDays[mChargingTargetSchedulesIdx][idx] = chargingTarget; + + idx++; + } + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR +ChargingTargetsMemMgr::AllocAndCopy(const DataModel::DecodableList & chargingTargets) +{ + // NOTE: ChargingTargetsMemMgr::PrepareDaySchedule() must be called as specified in the class comments in + // ChargingTargetsMemMgr.h before this method can be called. + + VerifyOrDie(mpListOfDays[mChargingTargetSchedulesIdx] == nullptr); + + size_t numDailyChargingTargets = 0; + ReturnErrorOnFailure(chargingTargets.ComputeSize(&numDailyChargingTargets)); + + mNumDailyChargingTargets = static_cast(numDailyChargingTargets); + + if (mNumDailyChargingTargets > 0) + { + // Allocate the memory first and then use placement new to initialise the memory of each element in the array + mpListOfDays[mChargingTargetSchedulesIdx] = static_cast( + chip::Platform::MemoryAlloc(sizeof(EnergyEvse::Structs::ChargingTargetStruct::Type) * mNumDailyChargingTargets)); + + VerifyOrReturnError(mpListOfDays[mChargingTargetSchedulesIdx] != nullptr, CHIP_ERROR_NO_MEMORY); + + uint16_t idx = 0; + auto it = chargingTargets.begin(); + while (it.Next()) + { + // Check that the idx is still valid + VerifyOrReturnError(idx < mNumDailyChargingTargets, CHIP_ERROR_INCORRECT_STATE); + + auto & chargingTarget = it.GetValue(); + + // This will cause the ChargingTargetStruct constructor to be called and this element in the array + new (mpListOfDays[mChargingTargetSchedulesIdx] + idx) EnergyEvse::Structs::ChargingTargetStruct::Type(); + + // Now copy the chargingTarget + mpListOfDays[mChargingTargetSchedulesIdx][idx] = chargingTarget; + + idx++; + } + } + + return CHIP_NO_ERROR; +} diff --git a/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp b/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp index 9006477b3662d5..00ced85a9aa2b6 100644 --- a/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp +++ b/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp @@ -19,7 +19,9 @@ #include #include #include +#include #include +#include #include #include @@ -39,7 +41,6 @@ using namespace chip::app::Clusters; using namespace chip::app::Clusters::EnergyEvse; using namespace chip::app::Clusters::ElectricalPowerMeasurement; using namespace chip::app::Clusters::ElectricalEnergyMeasurement; -using namespace chip::app::Clusters::ElectricalEnergyMeasurement::Structs; using namespace chip::app::Clusters::PowerSource; using namespace chip::app::Clusters::PowerSource::Attributes; @@ -115,6 +116,217 @@ CHIP_ERROR EVSEManufacturer::Shutdown() return CHIP_NO_ERROR; } +CHIP_ERROR FindNextTarget(const BitMask dayOfWeekMap, uint16_t minutesPastMidnightNow_m, + uint16_t & targetTimeMinutesPastMidnight_m, DataModel::Nullable & targetSoC, + DataModel::Nullable & addedEnergy_mWh, bool bAllowTargetsInPast) +{ + EnergyEvse::Structs::ChargingTargetScheduleStruct::Type entry; + + uint16_t minTimeToTarget_m = 24 * 60; // 24 hours + bool bFound = false; + + EVSEManufacturer * mn = GetEvseManufacturer(); + VerifyOrReturnError(mn != nullptr, CHIP_ERROR_UNINITIALIZED); + + EnergyEvseDelegate * dg = mn->GetEvseDelegate(); + VerifyOrReturnError(dg != nullptr, CHIP_ERROR_UNINITIALIZED); + + const DataModel::List & chargingTargetSchedules = + dg->GetEvseTargetsDelegate()->GetTargets(); + for (auto & chargingTargetScheduleEntry : chargingTargetSchedules) + { + if (chargingTargetScheduleEntry.dayOfWeekForSequence.HasAny(dayOfWeekMap)) + { + // We've found today's schedule - iterate through the targets on this day + for (auto & chargingTarget : chargingTargetScheduleEntry.chargingTargets) + { + if ((chargingTarget.targetTimeMinutesPastMidnight < minutesPastMidnightNow_m) && (bAllowTargetsInPast == false)) + { + // This target is in the past so move to the next if there is one + continue; + } + + if (chargingTarget.targetTimeMinutesPastMidnight < minTimeToTarget_m) + { + // This is the earliest target found in the day's targets so far + bFound = true; + minTimeToTarget_m = chargingTarget.targetTimeMinutesPastMidnight; + + targetTimeMinutesPastMidnight_m = chargingTarget.targetTimeMinutesPastMidnight; + + if (chargingTarget.targetSoC.HasValue()) + { + targetSoC.SetNonNull(chargingTarget.targetSoC.Value()); + } + else + { + targetSoC.SetNull(); + } + + if (chargingTarget.addedEnergy.HasValue()) + { + addedEnergy_mWh.SetNonNull(chargingTarget.addedEnergy.Value()); + } + else + { + addedEnergy_mWh.SetNull(); + } + } + } + } + + if (bFound) + { + // Skip the rest of the search + break; + } + } + + return bFound ? CHIP_NO_ERROR : CHIP_ERROR_NOT_FOUND; +} + +/** + * @brief Simple example to demonstrate how an EVSE can compute the start time + * and duration of a charging schedule + */ +CHIP_ERROR EVSEManufacturer::ComputeChargingSchedule() +{ + CHIP_ERROR err; + EVSEManufacturer * mn = GetEvseManufacturer(); + VerifyOrReturnError(mn != nullptr, CHIP_ERROR_UNINITIALIZED); + + EnergyEvseDelegate * dg = mn->GetEvseDelegate(); + VerifyOrReturnError(dg != nullptr, CHIP_ERROR_UNINITIALIZED); + + BitMask dayOfWeekMap = 0; + ReturnErrorOnFailure(GetLocalDayOfWeekNow(dayOfWeekMap)); + + uint16_t minutesPastMidnightNow_m = 0; + ReturnErrorOnFailure(GetMinutesPastMidnight(minutesPastMidnightNow_m)); + + uint32_t now_epoch_s = 0; + ReturnErrorOnFailure(GetEpochTS(now_epoch_s)); + + DataModel::Nullable startTime_epoch_s; + DataModel::Nullable targetTime_epoch_s; + DataModel::Nullable targetSoC; + DataModel::Nullable addedEnergy_mWh; + + uint32_t power_W; + uint32_t chargingDuration_s; + uint32_t tempTargetTime_epoch_s; + uint32_t tempStartTime_epoch_s; + uint16_t targetTimeMinutesPastMidnight_m; + + // Initialise the values to Null - if the FindNextTarget finds one, then it will update the value + targetTime_epoch_s.SetNull(); + targetSoC.SetNull(); + addedEnergy_mWh.SetNull(); + startTime_epoch_s.SetNull(); // If we FindNextTarget this will be computed below and set to a non null value + + /* We can only compute charging schedules if the EV is plugged in and the charging is enabled + * so we know the charging current - i.e. can get the max power, and therefore can calculate + * the charging duration and hence start time + */ + if (dg->IsEvsePluggedIn() && dg->GetSupplyState() == SupplyStateEnum::kChargingEnabled) + { + uint8_t searchDay = 0; + while (searchDay < 2) + { + err = FindNextTarget(dayOfWeekMap, minutesPastMidnightNow_m, targetTimeMinutesPastMidnight_m, targetSoC, + addedEnergy_mWh, (searchDay != 0)); + if (err == CHIP_ERROR_NOT_FOUND) + { + // We didn't find one for today, try tomorrow + searchDay++; + dayOfWeekMap = BitMask((dayOfWeekMap.Raw() << 1) & kAllTargetDaysMask); + + if (!dayOfWeekMap.HasAny()) + { + // Must be Saturday and shifted off, so set it to Sunday + dayOfWeekMap = BitMask(TargetDayOfWeekBitmap::kSunday); + } + } + else + { + break; // We found a target or we error'd out for some other reason + } + } + + if (err == CHIP_NO_ERROR) + { + /* Set the target Time in epoch_s format*/ + tempTargetTime_epoch_s = + ((now_epoch_s / 60) + targetTimeMinutesPastMidnight_m + (searchDay * 1440) - minutesPastMidnightNow_m) * 60; + targetTime_epoch_s.SetNonNull(tempTargetTime_epoch_s); + + if (!targetSoC.IsNull()) + { + if (targetSoC.Value() != 100) + { + ChipLogError(AppServer, "EVSE WARNING: TargetSoC is not 100%% and we don't know the EV SoC!"); + } + // We don't know the Vehicle SoC so we must charge now + // TODO make this use the SoC featureMap to determine if this is an error + startTime_epoch_s.SetNonNull(now_epoch_s); + } + else + { + // We expect to use AddedEnergy to determine the charging start time + if (addedEnergy_mWh.IsNull()) + { + ChipLogError(AppServer, "EVSE ERROR: Neither TargetSoC or AddedEnergy has been provided"); + return CHIP_ERROR_INTERNAL; + } + // Simple optimizer - assume a flat tariff throughout the day + // Compute power from nominal voltage and maxChargingRate + // GetMaximumChargeCurrent returns mA, but to help avoid overflow + // We use V (not mV) and compute power to the nearest Watt + power_W = static_cast((230 * dg->GetMaximumChargeCurrent()) / + 1000); // TODO don't use 230V - not all markets will use that + if (power_W == 0) + { + ChipLogError(AppServer, "EVSE Error: MaxCurrent = 0Amp - Can't schedule charging"); + return CHIP_ERROR_INTERNAL; + } + + // Time to charge(seconds) = (3600 * Energy(mWh) / Power(W)) / 1000 + // to avoid using floats we multiply by 36 and then divide by 10 (instead of x3600 and dividing by 1000) + chargingDuration_s = static_cast(((addedEnergy_mWh.Value() / power_W) * 36) / 10); + + // Add in 15 minutes leeway to account for slow starting vehicles + // that need to condition the battery or if it is cold etc + chargingDuration_s += (15 * 60); + + // A price optimizer can look for cheapest time of day + // However for now we'll start charging as late as possible + tempStartTime_epoch_s = tempTargetTime_epoch_s - chargingDuration_s; + + if (tempStartTime_epoch_s < now_epoch_s) + { + // we need to turn on the EVSE now - it won't have enough time to reach the target + startTime_epoch_s.SetNonNull(now_epoch_s); + // TODO call function to turn on the EV + } + else + { + // we turn off the EVSE for now + startTime_epoch_s.SetNonNull(tempStartTime_epoch_s); + // TODO have a periodic timer which checks if we should turn on the charger now + } + } + } + } + + // Update the attributes to allow a UI to inform the user + dg->SetNextChargeStartTime(startTime_epoch_s); + dg->SetNextChargeTargetTime(targetTime_epoch_s); + dg->SetNextChargeRequiredEnergy(addedEnergy_mWh); + dg->SetNextChargeTargetSoC(targetSoC); + + return err; +} + /** * @brief Allows a client application to initialise the Accuracy, Measurement types etc */ @@ -187,6 +399,8 @@ CHIP_ERROR EVSEManufacturer::SendPowerReading(EndpointId aEndpointId, int64_t aA return CHIP_NO_ERROR; } +using namespace chip::app::Clusters::ElectricalEnergyMeasurement::Structs; + /** * @brief Allows a client application to send cumulative energy readings into the system * @@ -349,10 +563,12 @@ void EVSEManufacturer::ApplicationCallbackHandler(const EVSECbInfo * cb, intptr_ { case EVSECallbackType::StateChanged: ChipLogProgress(AppServer, "EVSE callback - state changed"); + pClass->ComputeChargingSchedule(); break; case EVSECallbackType::ChargeCurrentChanged: ChipLogProgress(AppServer, "EVSE callback - maxChargeCurrent changed to %ld", static_cast(cb->ChargingCurrent.maximumChargeCurrent)); + pClass->ComputeChargingSchedule(); break; case EVSECallbackType::EnergyMeterReadingRequested: ChipLogProgress(AppServer, "EVSE callback - EnergyMeterReadingRequested"); @@ -366,6 +582,11 @@ void EVSEManufacturer::ApplicationCallbackHandler(const EVSECbInfo * cb, intptr_ } break; + case EVSECallbackType::ChargingPreferencesChanged: + ChipLogProgress(AppServer, "EVSE callback - ChargingPreferencesChanged"); + pClass->ComputeChargingSchedule(); + break; + default: ChipLogError(AppServer, "Unhandled EVSE Callback type %d", static_cast(cb->type)); } diff --git a/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp b/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp index b0cfd6fc5ce0ef..6266be245c2a4a 100644 --- a/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp +++ b/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp @@ -17,6 +17,7 @@ */ #include +#include #include #include #include @@ -42,13 +43,6 @@ EnergyEvseDelegate::~EnergyEvseDelegate() } } -/** - * @brief Helper function to get current timestamp in Epoch format - * - * @param chipEpoch reference to hold return timestamp - */ -CHIP_ERROR GetEpochTS(uint32_t & chipEpoch); - /** * @brief Called when EVSE cluster receives Disable command */ @@ -63,7 +57,9 @@ Status EnergyEvseDelegate::Disable() /* update MinimumChargeCurrent & MaximumChargeCurrent to 0 */ SetMinimumChargeCurrent(0); - SetMaximumChargeCurrent(0); + + mMaximumChargingCurrentLimitFromCommand = 0; + ComputeMaxChargeCurrentLimit(); /* update MaximumDischargeCurrent to 0 */ SetMaximumDischargeCurrent(0); @@ -83,13 +79,13 @@ Status EnergyEvseDelegate::EnableCharging(const DataModel::Nullable & { ChipLogProgress(AppServer, "EnergyEvseDelegate::EnableCharging()"); - if (maximumChargeCurrent < kMinimumChargeCurrent || maximumChargeCurrent > kMaximumChargeCurrent) + if (maximumChargeCurrent < kMinimumChargeCurrent) { ChipLogError(AppServer, "Maximum Current outside limits"); return Status::ConstraintError; } - if (minimumChargeCurrent < kMinimumChargeCurrent || minimumChargeCurrent > kMaximumChargeCurrent) + if (minimumChargeCurrent < kMinimumChargeCurrent) { ChipLogError(AppServer, "Maximum Current outside limits"); return Status::ConstraintError; @@ -177,7 +173,7 @@ Status EnergyEvseDelegate::ScheduleCheckOnEnabledTimeout() return Status::Success; } - CHIP_ERROR err = GetEpochTS(chipEpoch); + CHIP_ERROR err = DeviceEnergyManagement::GetEpochTS(chipEpoch); if (err == CHIP_NO_ERROR) { /* time is sync'd */ @@ -198,7 +194,7 @@ Status EnergyEvseDelegate::ScheduleCheckOnEnabledTimeout() else if (err == CHIP_ERROR_REAL_TIME_NOT_SYNCED) { /* Real time isn't sync'd -lets check again in 30 seconds - otherwise keep the charger enabled */ - DeviceLayer::SystemLayer().StartTimer(System::Clock::Seconds32(kPeriodicCheckIntervalRealTimeClockNotSynced), + DeviceLayer::SystemLayer().StartTimer(System::Clock::Seconds32(kPeriodicCheckIntervalRealTimeClockNotSynced_sec), EvseCheckTimerExpiry, this); } return Status::Success; @@ -234,6 +230,83 @@ Status EnergyEvseDelegate::StartDiagnostics() return Status::Success; } +/** + * @brief Called when EVSE cluster receives SetTargets command + */ +Status EnergyEvseDelegate::SetTargets( + const DataModel::DecodableList & chargingTargetSchedules) +{ + ChipLogProgress(AppServer, "EnergyEvseDelegate::SetTargets()"); + + EvseTargetsDelegate * targets = GetEvseTargetsDelegate(); + VerifyOrReturnError(targets != nullptr, Status::Failure); + + CHIP_ERROR err = targets->SetTargets(chargingTargetSchedules); + VerifyOrReturnError(err == CHIP_NO_ERROR, StatusIB(err).mStatus); + + /* The Application needs to be told that the Targets have been updated + * so it can potentially re-optimize the charging start time etc + */ + NotifyApplicationChargingPreferencesChange(); + + return Status::Success; +} + +Status EnergyEvseDelegate::LoadTargets() +{ + ChipLogProgress(AppServer, "EnergyEvseDelegate::LoadTargets()"); + + EvseTargetsDelegate * targets = GetEvseTargetsDelegate(); + VerifyOrReturnError(targets != nullptr, StatusIB(CHIP_ERROR_UNINITIALIZED).mStatus); + + CHIP_ERROR err = targets->LoadTargets(); + VerifyOrReturnError(err == CHIP_NO_ERROR, StatusIB(err).mStatus); + + return Status::Success; +} + +Status EnergyEvseDelegate::GetTargets(DataModel::List & chargingTargetSchedules) +{ + ChipLogProgress(AppServer, "EnergyEvseDelegate::GetTargets()"); + + EvseTargetsDelegate * targets = GetEvseTargetsDelegate(); + VerifyOrReturnError(targets != nullptr, StatusIB(CHIP_ERROR_UNINITIALIZED).mStatus); + + chargingTargetSchedules = targets->GetTargets(); + + return Status::Success; +} + +/** + * @brief Called when EVSE cluster receives ClearTargets command + */ +Status EnergyEvseDelegate::ClearTargets() +{ + ChipLogProgress(AppServer, "EnergyEvseDelegate::ClearTargets()"); + + CHIP_ERROR err; + + EvseTargetsDelegate * targets = GetEvseTargetsDelegate(); + if (targets == nullptr) + { + return StatusIB(CHIP_ERROR_UNINITIALIZED).mStatus; + } + + err = targets->ClearTargets(); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "Failed to clear Evse targets: %" CHIP_ERROR_FORMAT, err.Format()); + return Status::Failure; + } + + /* The Application needs to be told that the Targets have been deleted + * so it can potentially re-optimize the charging start time etc + */ + NotifyApplicationChargingPreferencesChange(); + + return Status::Success; +} + /* --------------------------------------------------------------------------- * EVSE Hardware interface below */ @@ -269,7 +342,7 @@ Status EnergyEvseDelegate::HwRegisterEvseCallbackHandler(EVSECallbackFunc handle */ Status EnergyEvseDelegate::HwSetMaxHardwareCurrentLimit(int64_t currentmA) { - if (currentmA < kMinimumChargeCurrent || currentmA > kMaximumChargeCurrent) + if (currentmA < kMinimumChargeCurrent) { return Status::ConstraintError; } @@ -291,7 +364,7 @@ Status EnergyEvseDelegate::HwSetMaxHardwareCurrentLimit(int64_t currentmA) */ Status EnergyEvseDelegate::HwSetCircuitCapacity(int64_t currentmA) { - if (currentmA < kMinimumChargeCurrent || currentmA > kMaximumChargeCurrent) + if (currentmA < kMinimumChargeCurrent) { return Status::ConstraintError; } @@ -316,7 +389,7 @@ Status EnergyEvseDelegate::HwSetCircuitCapacity(int64_t currentmA) */ Status EnergyEvseDelegate::HwSetCableAssemblyLimit(int64_t currentmA) { - if (currentmA < kMinimumChargeCurrent || currentmA > kMaximumChargeCurrent) + if (currentmA < kMinimumChargeCurrent) { return Status::ConstraintError; } @@ -909,6 +982,20 @@ Status EnergyEvseDelegate::NotifyApplicationStateChange() return Status::Success; } +Status EnergyEvseDelegate::NotifyApplicationChargingPreferencesChange() +{ + EVSECbInfo cbInfo; + + cbInfo.type = EVSECallbackType::ChargingPreferencesChanged; + + if (mCallbacks.handler != nullptr) + { + mCallbacks.handler(&cbInfo, mCallbacks.arg); + } + + return Status::Success; +} + Status EnergyEvseDelegate::GetEVSEEnergyMeterValue(ChargingDischargingType meterType, int64_t & aMeterValue) { EVSECbInfo cbInfo; @@ -1227,7 +1314,7 @@ CHIP_ERROR EnergyEvseDelegate::SetCircuitCapacity(int64_t newValue) { int64_t oldValue = mCircuitCapacity; - if (newValue >= kMaximumChargeCurrent) + if (newValue < 0) { return CHIP_IM_GLOBAL_STATUS(ConstraintError); } @@ -1251,7 +1338,7 @@ CHIP_ERROR EnergyEvseDelegate::SetMinimumChargeCurrent(int64_t newValue) { int64_t oldValue = mMinimumChargeCurrent; - if (newValue >= kMaximumChargeCurrent) + if (newValue < 0) { return CHIP_IM_GLOBAL_STATUS(ConstraintError); } @@ -1271,24 +1358,6 @@ int64_t EnergyEvseDelegate::GetMaximumChargeCurrent() return mMaximumChargeCurrent; } -CHIP_ERROR EnergyEvseDelegate::SetMaximumChargeCurrent(int64_t newValue) -{ - int64_t oldValue = mMaximumChargeCurrent; - - if (newValue >= kMaximumChargeCurrent) - { - return CHIP_IM_GLOBAL_STATUS(ConstraintError); - } - - mMaximumChargeCurrent = newValue; - if (oldValue != mMaximumChargeCurrent) - { - ChipLogDetail(AppServer, "MaximumChargeCurrent updated to %ld", static_cast(mMaximumChargeCurrent)); - MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, MaximumChargeCurrent::Id); - } - return CHIP_NO_ERROR; -} - /* MaximumDischargeCurrent */ int64_t EnergyEvseDelegate::GetMaximumDischargeCurrent() { @@ -1299,7 +1368,7 @@ CHIP_ERROR EnergyEvseDelegate::SetMaximumDischargeCurrent(int64_t newValue) { int64_t oldValue = mMaximumDischargeCurrent; - if (newValue >= kMaximumChargeCurrent) + if (newValue < 0) { return CHIP_IM_GLOBAL_STATUS(ConstraintError); } @@ -1321,7 +1390,7 @@ int64_t EnergyEvseDelegate::GetUserMaximumChargeCurrent() CHIP_ERROR EnergyEvseDelegate::SetUserMaximumChargeCurrent(int64_t newValue) { - if ((newValue < 0) || (newValue > kMaximumChargeCurrent)) + if (newValue < 0) { return CHIP_IM_GLOBAL_STATUS(ConstraintError); } @@ -1378,18 +1447,105 @@ DataModel::Nullable EnergyEvseDelegate::GetNextChargeStartTime() { return mNextChargeStartTime; } +CHIP_ERROR EnergyEvseDelegate::SetNextChargeStartTime(DataModel::Nullable newNextChargeStartTimeUtc) +{ + if (newNextChargeStartTimeUtc == mNextChargeStartTime) + { + return CHIP_NO_ERROR; + } + + mNextChargeStartTime = newNextChargeStartTimeUtc; + if (mNextChargeStartTime.IsNull()) + { + ChipLogDetail(AppServer, "NextChargeStartTime updated to Null"); + } + else + { + ChipLogDetail(AppServer, "NextChargeStartTime updated to %lu", + static_cast(mNextChargeStartTime.Value())); + } + + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, NextChargeStartTime::Id); + + return CHIP_NO_ERROR; +} + DataModel::Nullable EnergyEvseDelegate::GetNextChargeTargetTime() { return mNextChargeTargetTime; } +CHIP_ERROR EnergyEvseDelegate::SetNextChargeTargetTime(DataModel::Nullable newNextChargeTargetTimeUtc) +{ + if (newNextChargeTargetTimeUtc == mNextChargeTargetTime) + { + return CHIP_NO_ERROR; + } + + mNextChargeTargetTime = newNextChargeTargetTimeUtc; + if (mNextChargeTargetTime.IsNull()) + { + ChipLogDetail(AppServer, "NextChargeTargetTime updated to Null"); + } + else + { + ChipLogDetail(AppServer, "NextChargeTargetTime updated to %lu", + static_cast(mNextChargeTargetTime.Value())); + } + + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, NextChargeTargetTime::Id); + + return CHIP_NO_ERROR; +} + DataModel::Nullable EnergyEvseDelegate::GetNextChargeRequiredEnergy() { return mNextChargeRequiredEnergy; } +CHIP_ERROR EnergyEvseDelegate::SetNextChargeRequiredEnergy(DataModel::Nullable newNextChargeRequiredEnergyMilliWattH) +{ + if (mNextChargeRequiredEnergy == newNextChargeRequiredEnergyMilliWattH) + { + return CHIP_NO_ERROR; + } + + mNextChargeRequiredEnergy = newNextChargeRequiredEnergyMilliWattH; + if (mNextChargeRequiredEnergy.IsNull()) + { + ChipLogDetail(AppServer, "NextChargeRequiredEnergy updated to Null"); + } + else + { + ChipLogDetail(AppServer, "NextChargeRequiredEnergy updated to %ld", static_cast(mNextChargeRequiredEnergy.Value())); + } + + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, NextChargeRequiredEnergy::Id); + + return CHIP_NO_ERROR; +} + DataModel::Nullable EnergyEvseDelegate::GetNextChargeTargetSoC() { return mNextChargeTargetSoC; } +CHIP_ERROR EnergyEvseDelegate::SetNextChargeTargetSoC(DataModel::Nullable newValue) +{ + DataModel::Nullable oldValue = mNextChargeTargetSoC; + + mNextChargeTargetSoC = newValue; + if (oldValue != newValue) + { + if (newValue.IsNull()) + { + ChipLogDetail(AppServer, "NextChargeTargetSoC updated to Null"); + } + else + { + ChipLogDetail(AppServer, "NextChargeTargetSoC updated to %d %%", mNextChargeTargetSoC.Value()); + } + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, NextChargeTargetSoC::Id); + } + return CHIP_NO_ERROR; +} /* ApproximateEVEfficiency */ DataModel::Nullable EnergyEvseDelegate::GetApproximateEVEfficiency() @@ -1402,7 +1558,7 @@ CHIP_ERROR EnergyEvseDelegate::SetApproximateEVEfficiency(DataModel::Nullable oldValue = mApproximateEVEfficiency; mApproximateEVEfficiency = newValue; - if ((oldValue != newValue)) + if (oldValue != newValue) { if (newValue.IsNull()) { @@ -1458,42 +1614,13 @@ DataModel::Nullable EnergyEvseDelegate::GetSessionEnergyDischarged() } /** - * @brief Helper function to get current timestamp in Epoch format - * - * @param chipEpoch reference to hold return timestamp + * @brief Helper function to get know if the EV is plugged in based on state + * (regardless of if it is actually transferring energy) */ -CHIP_ERROR GetEpochTS(uint32_t & chipEpoch) +bool EnergyEvseDelegate::IsEvsePluggedIn() { - chipEpoch = 0; - - System::Clock::Milliseconds64 cTMs; - CHIP_ERROR err = System::SystemClock().GetClock_RealTimeMS(cTMs); - - /* If the GetClock_RealTimeMS returns CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE, then - * This platform cannot ever report real time ! - * This should not be certifiable since getting time is a Mandatory - * feature of EVSE Cluster - */ - if (err == CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE) - { - ChipLogError(Zcl, "Platform does not support GetClock_RealTimeMS. Check EVSE certification requirements!"); - return err; - } - - if (err != CHIP_NO_ERROR) - { - ChipLogError(Zcl, "EVSE: Unable to get current time - err:%" CHIP_ERROR_FORMAT, err.Format()); - return err; - } - - auto unixEpoch = std::chrono::duration_cast(cTMs).count(); - if (!UnixEpochToChipEpochTime(unixEpoch, chipEpoch)) - { - ChipLogError(Zcl, "EVSE: unable to convert Unix Epoch time to Matter Epoch Time"); - return err; - } - - return CHIP_NO_ERROR; + return (mState == StateEnum::kPluggedInCharging || mState == StateEnum::kPluggedInDemand || + mState == StateEnum::kPluggedInDischarging || mState == StateEnum::kPluggedInNoDemand); } /** @@ -1506,7 +1633,7 @@ void EvseSession::StartSession(int64_t chargingMeterValue, int64_t dischargingMe { /* Get Timestamp */ uint32_t chipEpoch = 0; - CHIP_ERROR err = GetEpochTS(chipEpoch); + CHIP_ERROR err = DeviceEnergyManagement::GetEpochTS(chipEpoch); if (err != CHIP_NO_ERROR) { /* Note that the error will be also be logged inside GetErrorTS() - @@ -1548,6 +1675,8 @@ void EvseSession::StartSession(int64_t chargingMeterValue, int64_t dischargingMe // TODO persist mSessionEnergyDischargedAtStart } +/*---------------------- EvseSession functions --------------------------*/ + /** * @brief This function updates the session attrs to allow read attributes to return latest values */ @@ -1555,7 +1684,7 @@ void EvseSession::RecalculateSessionDuration() { /* Get Timestamp */ uint32_t chipEpoch = 0; - CHIP_ERROR err = GetEpochTS(chipEpoch); + CHIP_ERROR err = DeviceEnergyManagement::GetEpochTS(chipEpoch); if (err != CHIP_NO_ERROR) { /* Note that the error will be also be logged inside GetErrorTS() - diff --git a/examples/energy-management-app/energy-management-common/src/EnergyEvseEventTriggers.cpp b/examples/energy-management-app/energy-management-common/src/EnergyEvseEventTriggers.cpp index 62329068610f47..a164bb0bafb341 100644 --- a/examples/energy-management-app/energy-management-common/src/EnergyEvseEventTriggers.cpp +++ b/examples/energy-management-app/energy-management-common/src/EnergyEvseEventTriggers.cpp @@ -101,6 +101,14 @@ void SetTestEventTrigger_EVChargeDemandClear() dg->HwSetState(sEVSETestEventSaveData.mOldHwStatePluggedInDemand); } +void SetTestEventTrigger_EVTimeOfUseMode() +{ + // TODO - See #34249 +} +void SetTestEventTrigger_EVTimeOfUseModeClear() +{ + // TODO - See #34249 +} void SetTestEventTrigger_EVSEGroundFault() { EnergyEvseDelegate * dg = GetEvseDelegate(); @@ -159,6 +167,10 @@ bool HandleEnergyEvseTestEventTrigger(uint64_t eventTrigger) ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => EV Charge NoDemand"); SetTestEventTrigger_EVChargeDemandClear(); break; + case EnergyEvseTrigger::kEVTimeOfUseMode: + ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => EV TimeOfUse Mode"); + SetTestEventTrigger_EVTimeOfUseMode(); + break; case EnergyEvseTrigger::kEVSEGroundFault: ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => EVSE has a GroundFault fault"); SetTestEventTrigger_EVSEGroundFault(); @@ -175,7 +187,10 @@ bool HandleEnergyEvseTestEventTrigger(uint64_t eventTrigger) ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => EVSE Diagnostics Completed"); SetTestEventTrigger_EVSEDiagnosticsComplete(); break; - + case EnergyEvseTrigger::kEVTimeOfUseModeClear: + ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => EV TimeOfUse Mode clear"); + SetTestEventTrigger_EVTimeOfUseModeClear(); + break; default: return false; } diff --git a/examples/energy-management-app/energy-management-common/src/EnergyEvseMain.cpp b/examples/energy-management-app/energy-management-common/src/EnergyEvseMain.cpp index 0a4bc0456e2254..bd22c67eafd9b5 100644 --- a/examples/energy-management-app/energy-management-common/src/EnergyEvseMain.cpp +++ b/examples/energy-management-app/energy-management-common/src/EnergyEvseMain.cpp @@ -49,6 +49,7 @@ using namespace chip::app::Clusters::ElectricalEnergyMeasurement; using namespace chip::app::Clusters::PowerTopology; static std::unique_ptr gEvseDelegate; +static std::unique_ptr gEvseTargetsDelegate; static std::unique_ptr gEvseInstance; static std::unique_ptr gDEMDelegate; static std::unique_ptr gDEMInstance; @@ -143,16 +144,24 @@ CHIP_ERROR EnergyEvseInit() { CHIP_ERROR err; - if (gEvseDelegate || gEvseInstance) + if (gEvseDelegate || gEvseInstance || gEvseTargetsDelegate) { - ChipLogError(AppServer, "EVSE Instance or Delegate already exist."); + ChipLogError(AppServer, "EVSE Instance, Delegate or TargetsDelegate already exist."); return CHIP_ERROR_INCORRECT_STATE; } - gEvseDelegate = std::make_unique(); + gEvseTargetsDelegate = std::make_unique(); + if (!gEvseTargetsDelegate) + { + ChipLogError(AppServer, "Failed to allocate memory for EvseTargetsDelegate"); + return CHIP_ERROR_NO_MEMORY; + } + + gEvseDelegate = std::make_unique(*gEvseTargetsDelegate); if (!gEvseDelegate) { ChipLogError(AppServer, "Failed to allocate memory for EnergyEvseDelegate"); + gEvseTargetsDelegate.reset(); return CHIP_ERROR_NO_MEMORY; } @@ -168,6 +177,7 @@ CHIP_ERROR EnergyEvseInit() if (!gEvseInstance) { ChipLogError(AppServer, "Failed to allocate memory for EnergyEvseManager"); + gEvseTargetsDelegate.reset(); gEvseDelegate.reset(); return CHIP_ERROR_NO_MEMORY; } @@ -176,6 +186,17 @@ CHIP_ERROR EnergyEvseInit() if (err != CHIP_NO_ERROR) { ChipLogError(AppServer, "Init failed on gEvseInstance"); + gEvseTargetsDelegate.reset(); + gEvseInstance.reset(); + gEvseDelegate.reset(); + return err; + } + + err = gEvseTargetsDelegate->LoadTargets(); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "Failed to LoadTargets"); + gEvseTargetsDelegate.reset(); gEvseInstance.reset(); gEvseDelegate.reset(); return err; diff --git a/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp b/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp index c8b43342f8bd1c..52a8fec55673dd 100644 --- a/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp +++ b/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp @@ -18,6 +18,7 @@ #include #include +#include using namespace chip::app; using namespace chip::app::Clusters; @@ -117,6 +118,16 @@ CHIP_ERROR EnergyEvseManager::LoadPersistentAttributes() CHIP_ERROR EnergyEvseManager::Init() { ReturnErrorOnFailure(Instance::Init()); + + // Set up the EnergyEvseTargetsStore and persistent storage delegate + EnergyEvseDelegate * dg = GetDelegate(); + VerifyOrReturnLogError(dg != nullptr, CHIP_ERROR_UNINITIALIZED); + + EvseTargetsDelegate * targetsStore = dg->GetEvseTargetsDelegate(); + VerifyOrReturnLogError(targetsStore != nullptr, CHIP_ERROR_UNINITIALIZED); + + ReturnErrorOnFailure(targetsStore->Init(&Server::GetInstance().GetPersistentStorage())); + return LoadPersistentAttributes(); } diff --git a/examples/energy-management-app/energy-management-common/src/EnergyEvseTargetsStore.cpp b/examples/energy-management-app/energy-management-common/src/EnergyEvseTargetsStore.cpp new file mode 100644 index 00000000000000..f15e0b7a109817 --- /dev/null +++ b/examples/energy-management-app/energy-management-common/src/EnergyEvseTargetsStore.cpp @@ -0,0 +1,489 @@ +/* + * + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::app::DataModel; +using namespace chip::app::Clusters; +using namespace chip::app::Clusters::EnergyEvse; +using chip::Protocols::InteractionModel::Status; + +EvseTargetsDelegate::EvseTargetsDelegate() {} + +EvseTargetsDelegate::~EvseTargetsDelegate() {} + +CHIP_ERROR EvseTargetsDelegate::Init(PersistentStorageDelegate * targetStore) +{ + ChipLogProgress(AppServer, "EVSE: Initializing EvseTargetsDelegate"); + VerifyOrReturnError(targetStore != nullptr, CHIP_ERROR_INVALID_ARGUMENT); + + mpTargetStore = targetStore; + + // Set FabricDelegate + chip::Server::GetInstance().GetFabricTable().AddFabricDelegate(this); + + return CHIP_NO_ERROR; +} + +const DataModel::List & EvseTargetsDelegate::GetTargets() +{ + return mChargingTargetSchedulesList; +} + +/* static */ +uint16_t EvseTargetsDelegate::GetTlvSizeUpperBound() +{ + size_t kListOverhead = 4; + size_t chargingTargetStuctEstimate = + TLV::EstimateStructOverhead(sizeof(uint16_t), sizeof(Optional), sizeof(Optional)); + size_t chargingTargetScheduleStructEstimate = TLV::EstimateStructOverhead(sizeof(chip::BitMask)) + + kListOverhead + kEvseTargetsMaxTargetsPerDay * chargingTargetStuctEstimate; + size_t totalEstimate = kEvseTargetsMaxNumberOfDays * chargingTargetScheduleStructEstimate + kListOverhead; + + return static_cast(totalEstimate); +} + +CHIP_ERROR EvseTargetsDelegate::LoadTargets() +{ + // The DataModel::List data structure contains a list of + // ChargingTargetScheduleStructs which in turn contains a list of ChargingTargetStructs. Lists contain pointers + // to objects allocated outside of the List. For mChargingTargetSchedulesList, that memory is allocated in + // mChargingTargets and mChargingTargetSchedulesArray. + + Platform::ScopedMemoryBuffer backingBuffer; + uint16_t length = GetTlvSizeUpperBound(); + ReturnErrorCodeIf(!backingBuffer.Calloc(length), CHIP_ERROR_NO_MEMORY); + + CHIP_ERROR err = mpTargetStore->SyncGetKeyValue(spEvseTargetsKeyName, backingBuffer.Get(), length); + if (err == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND) + { + // Targets does not exist persistent storage -> initialise mChargingTargetSchedulesList as empty + mChargingTargetSchedulesList = DataModel::List(); + + return CHIP_NO_ERROR; + } + ReturnErrorOnFailure(err); + + TLV::ScopedBufferTLVReader reader(std::move(backingBuffer), length); + + ReturnErrorOnFailure(reader.Next(TLV::kTLVType_Array, TLV::AnonymousTag())); + TLV::TLVType arrayType; + ReturnErrorOnFailure(reader.EnterContainer(arrayType)); + + uint16_t chargingTargetSchedulesIdx = 0; + while ((err = reader.Next(TLV::kTLVType_Structure, TLV::AnonymousTag())) == CHIP_NO_ERROR) + { + TLV::TLVType evseTargetEntryType; + + ReturnErrorOnFailure(reader.EnterContainer(evseTargetEntryType)); + + // Check we are not exceeding the size of the mChargingTargetSchedulesArray + VerifyOrReturnError(chargingTargetSchedulesIdx < kEvseTargetsMaxNumberOfDays, CHIP_ERROR_INCORRECT_STATE); + + // DayOfWeek bitmap + ReturnErrorOnFailure(reader.Next(TLV::ContextTag(TargetEntryTag::kDayOfWeek))); + ReturnErrorOnFailure(reader.Get(mChargingTargetSchedulesArray[chargingTargetSchedulesIdx].dayOfWeekForSequence)); + + ChipLogProgress(AppServer, "LoadTargets: DayOfWeekForSequence = 0x%02x", + mChargingTargetSchedulesArray[chargingTargetSchedulesIdx].dayOfWeekForSequence.GetField( + static_cast(kAllTargetDaysMask))); + + // ChargingTargets List + ReturnErrorOnFailure(reader.Next(TLV::kTLVType_List, TLV::ContextTag(TargetEntryTag::kChargingTargetsList))); + TLV::TLVType chargingTargetsListType; + ReturnErrorOnFailure(reader.EnterContainer(chargingTargetsListType)); + + // The mChargingTargets object handles the allocation of the chargingTargets. Let it know the currentSchedule index + mChargingTargets.PrepareDaySchedule(chargingTargetSchedulesIdx); + + // Load the chargingTargets associated with this schedule + while ((err = reader.Next(TLV::kTLVType_Structure, TLV::ContextTag(TargetEntryTag::kChargingTargetsStruct))) == + CHIP_NO_ERROR) + { + TLV::TLVType chargingTargetsStructType = TLV::kTLVType_Structure; + ReturnErrorOnFailure(reader.EnterContainer(chargingTargetsStructType)); + + // Keep track of the current chargingTarget being loaded + EnergyEvse::Structs::ChargingTargetStruct::Type chargingTarget; + + while ((err = reader.Next()) == CHIP_NO_ERROR) + { + auto type = reader.GetType(); + auto tag = reader.GetTag(); + if (type == TLV::kTLVType_NotSpecified) + { + // Something wrong - we've lost alignment + return CHIP_ERROR_UNEXPECTED_TLV_ELEMENT; + } + + if (tag == TLV::ContextTag(TargetEntryTag::kTargetTime)) + { + ReturnErrorOnFailure(reader.Get(chargingTarget.targetTimeMinutesPastMidnight)); + } + else if (tag == TLV::ContextTag(TargetEntryTag::kTargetSoC)) + { + chip::Percent tempSoC; + ReturnErrorOnFailure(reader.Get(tempSoC)); + chargingTarget.targetSoC.SetValue(tempSoC); + } + else if (tag == TLV::ContextTag(TargetEntryTag::kAddedEnergy)) + { + int64_t tempAddedEnergy; + ReturnErrorOnFailure(reader.Get(tempAddedEnergy)); + chargingTarget.addedEnergy.SetValue(tempAddedEnergy); + } + else + { + // Something else unexpected here + return CHIP_ERROR_UNEXPECTED_TLV_ELEMENT; + } + } + + ReturnErrorOnFailure(reader.ExitContainer(chargingTargetsStructType)); + + ChipLogProgress(AppServer, + "LoadingTargets: targetTimeMinutesPastMidnight %u targetSoC %u addedEnergy 0x" ChipLogFormatX64, + chargingTarget.targetTimeMinutesPastMidnight, chargingTarget.targetSoC.ValueOr(0), + ChipLogValueX64(chargingTarget.addedEnergy.ValueOr(0))); + + // Update mChargingTargets which is tracking the chargingTargets + mChargingTargets.AddChargingTarget(chargingTarget); + } + + ReturnErrorOnFailure(reader.ExitContainer(chargingTargetsListType)); + ReturnErrorOnFailure(reader.ExitContainer(evseTargetEntryType)); + + // Allocate an array for the chargingTargets loaded for this schedule and copy the chargingTargets into that array. + // The allocated array will be pointed to in the List below. + err = mChargingTargets.AllocAndCopy(); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "SetTargets: Failed to allocate memory during LoadTargets %s", chip::ErrorStr(err)); + return err; + } + + // Construct the List. mChargingTargetSchedulesArray will be pointed to in the + // List mChargingTargetSchedulesList below + mChargingTargetSchedulesArray[chargingTargetSchedulesIdx].chargingTargets = + chip::app::DataModel::List( + mChargingTargets.GetChargingTargets(), mChargingTargets.GetNumDailyChargingTargets()); + + chargingTargetSchedulesIdx++; + } + + ReturnErrorOnFailure(reader.ExitContainer(arrayType)); + + // Finalise mChargingTargetSchedulesList + mChargingTargetSchedulesList = DataModel::List(mChargingTargetSchedulesArray, + chargingTargetSchedulesIdx); + + return reader.VerifyEndOfContainer(); +} + +/** + * This function tries to compress a list of entries which has: + * dayOfWeek bitmask + * chargingTargetsList + * + * It takes a new entry and scans the existing list to see if the + * dayOfWeek bitmask is already included somewhere + * + * compute bitmask values: + * + * bitmaskA: (entry.bitmask & bitmask) + * work out which bits in the existing entry are the same (overlapping) + * + * Create and append a new entry for the bits that are the same + * newEntry.bitmask = bitmaskA; + * newEntry.chargingTargetsList = chargingTargetsList + * + * if entry.bitmask == bitmaskA + * this entry is being deleted and can share the newEntry + * delete it + * + * bitmaskB = (entry.bitmask & ~bitmask); + * work out which bits in the existing entry are different + * Remove these bits from the existing entry, (effectively deleting them) + * entry.bitmask = bitmaskB; + * + * NOTE: if `all` bits are removed then the existing entry can be deleted, + * but that's not possible because we check for a full match first + * + * We continue walking our list to see if other entries have overlapping bits + * If they do, then the newEntry.bitmask |= bitmaskA + * + */ +CHIP_ERROR EvseTargetsDelegate::SetTargets( + const DataModel::DecodableList & newChargingTargetSchedules) +{ + ChipLogProgress(AppServer, "SetTargets"); + + // We'll need to have a local copy of the chargingTargets that are referenced from updatedChargingTargetSchedules (which + // is a List where each ChargingTargetScheduleStruct has a List of ChargingTargetStructs). + // Note updatedChargingTargets only needs to exist for the duration of this method as once the new targets have been merged + // with the existing targets, we'll save the updated Targets structure to persistent storage and the reload it into + // mChargingTargetSchedulesList + ChargingTargetsMemMgr updatedChargingTargets; + + // Build up a new Targets structure + DataModel::DecodableList updatedChargingTargetSchedules; + + // updatedChargingTargetSchedules contains a List of ChargingTargetScheduleStruct where the memory of + // ChargingTargetScheduleStruct is which is allocated here. + Structs::ChargingTargetScheduleStruct::Type updatedChargingTargetSchedulesArray[kEvseTargetsMaxNumberOfDays]; + + // Iterate across the list of new schedules. For each schedule, iterate through the existing Target + // (mChargingTargetSchedulesList) working out how to merge the new schedule. + auto newIter = newChargingTargetSchedules.begin(); + while (newIter.Next()) + { + auto & newChargingTargetSchedule = newIter.GetValue(); + + uint8_t newBitmask = + newChargingTargetSchedule.dayOfWeekForSequence.GetField(static_cast(kAllTargetDaysMask)); + + ChipLogProgress(AppServer, "SetTargets: DayOfWeekForSequence = 0x%02x", newBitmask); + + PrintTargets(mChargingTargetSchedulesList); + + // Iterate across the existing schedule entries, seeing if there is overlap with + // the dayOfWeekForSequenceBitmap + bool found = false; + uint16_t updatedChargingTargetSchedulesIdx = 0; + + // Let the updatedChargingTargets object of the schedule index + updatedChargingTargets.PrepareDaySchedule(updatedChargingTargetSchedulesIdx); + + for (auto & currentChargingTargetSchedule : mChargingTargetSchedulesList) + { + uint8_t currentBitmask = + currentChargingTargetSchedule.dayOfWeekForSequence.GetField(static_cast(kAllTargetDaysMask)); + + ChipLogProgress(AppServer, "SetTargets: Scanning current entry %d of %d: bitmap 0x%02x", + updatedChargingTargetSchedulesIdx, static_cast(mChargingTargetSchedulesList.size()), + currentBitmask); + + // Work out if the new schedule dayOfWeekSequence overlaps with any existing schedules + uint8_t bitmaskA = static_cast(currentBitmask & newBitmask); + uint8_t bitmaskB = static_cast(currentBitmask & ~newBitmask); + + BitMask updatedBitmask; + + if (currentBitmask == bitmaskA) + { + // This entry has the all the same bits as the newEntry + updatedBitmask = BitMask(bitmaskA); + + // Copy the new chargingTargets to this schedule index + CHIP_ERROR err = updatedChargingTargets.AllocAndCopy(newChargingTargetSchedule.chargingTargets); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "SetTargets: Failed to copy the new chargingTargets %s", chip::ErrorStr(err)); + return err; + } + + found = true; + } + else + { + // This entry stays - but it has lost some days from the bitmask + updatedBitmask = BitMask(bitmaskB); + + // Copy the existing chargingTargets + CHIP_ERROR err = updatedChargingTargets.AllocAndCopy(currentChargingTargetSchedule.chargingTargets); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "SetTargets: Failed to copy the new chargingTargets %s", chip::ErrorStr(err)); + return err; + } + } + + // Update the new schedule with the dayOfWeekForSequence and list of chargingTargets + updatedChargingTargetSchedulesArray[updatedChargingTargetSchedulesIdx].dayOfWeekForSequence = updatedBitmask; + + updatedChargingTargetSchedulesArray[updatedChargingTargetSchedulesIdx].chargingTargets = + chip::app::DataModel::List( + updatedChargingTargets.GetChargingTargets(), updatedChargingTargets.GetNumDailyChargingTargets()); + + // Going to look at the next schedule entry + updatedChargingTargetSchedulesIdx++; + + // Let the updatedChargingTargets object of the schedule index + updatedChargingTargets.PrepareDaySchedule(updatedChargingTargetSchedulesIdx); + } + + // If found is false, then there were no existing entries for the dayOfWeekForSequence. Add a new entry + if (!found) + { + // Copy the new chargingTargets + CHIP_ERROR err = updatedChargingTargets.AllocAndCopy(newChargingTargetSchedule.chargingTargets); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "SetTargets: Failed to copy the new chargingTargets %s", chip::ErrorStr(err)); + return err; + } + + // Update the new schedule with the dayOfWeekForSequence and list of chargingTargets + updatedChargingTargetSchedulesArray[updatedChargingTargetSchedulesIdx].dayOfWeekForSequence = + newChargingTargetSchedule.dayOfWeekForSequence; + + updatedChargingTargetSchedulesArray[updatedChargingTargetSchedulesIdx].chargingTargets = + chip::app::DataModel::List( + updatedChargingTargets.GetChargingTargets(), updatedChargingTargets.GetNumDailyChargingTargets()); + + // We've added a new schedule entry + updatedChargingTargetSchedulesIdx++; + + // Let the updatedChargingTargets object of the schedule index + updatedChargingTargets.PrepareDaySchedule(updatedChargingTargetSchedulesIdx); + } + + // Now create the full Target data structure that we are going to save to persistent storage + DataModel::List updatedChargingTargetSchedulesList( + updatedChargingTargetSchedulesArray, updatedChargingTargetSchedulesIdx); + + CHIP_ERROR err = SaveTargets(updatedChargingTargetSchedulesList); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "SetTargets: Failed to save Target to persistent storage %s", chip::ErrorStr(err)); + return err; + } + + // Now reload from persistent storage so that mChargingTargetSchedulesList gets the update Target + err = LoadTargets(); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "SetTargets: Failed to load Target from persistent storage %s", chip::ErrorStr(err)); + return err; + } + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR +EvseTargetsDelegate::SaveTargets(DataModel::List & chargingTargetSchedulesList) +{ + uint16_t total = GetTlvSizeUpperBound(); + + Platform::ScopedMemoryBuffer backingBuffer; + ReturnErrorCodeIf(!backingBuffer.Calloc(total), CHIP_ERROR_NO_MEMORY); + TLV::ScopedBufferTLVWriter writer(std::move(backingBuffer), total); + + TLV::TLVType arrayType; + ReturnErrorOnFailure(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, arrayType)); + for (auto & chargingTargetSchedule : chargingTargetSchedulesList) + { + ChipLogProgress( + AppServer, "SaveTargets: DayOfWeekForSequence = 0x%02x", + chargingTargetSchedule.dayOfWeekForSequence.GetField(static_cast(kAllTargetDaysMask))); + + TLV::TLVType evseTargetEntryType; + ReturnErrorOnFailure(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, evseTargetEntryType)); + ReturnErrorOnFailure(writer.Put(TLV::ContextTag(TargetEntryTag::kDayOfWeek), chargingTargetSchedule.dayOfWeekForSequence)); + + TLV::TLVType chargingTargetsListType; + ReturnErrorOnFailure(writer.StartContainer(TLV::ContextTag(TargetEntryTag::kChargingTargetsList), TLV::kTLVType_List, + chargingTargetsListType)); + for (auto & chargingTarget : chargingTargetSchedule.chargingTargets) + { + TLV::TLVType chargingTargetsStructType = TLV::kTLVType_Structure; + ReturnErrorOnFailure(writer.StartContainer(TLV::ContextTag(TargetEntryTag::kChargingTargetsStruct), + TLV::kTLVType_Structure, chargingTargetsStructType)); + ReturnErrorOnFailure( + writer.Put(TLV::ContextTag(TargetEntryTag::kTargetTime), chargingTarget.targetTimeMinutesPastMidnight)); + if (chargingTarget.targetSoC.HasValue()) + { + ReturnErrorOnFailure(writer.Put(TLV::ContextTag(TargetEntryTag::kTargetSoC), chargingTarget.targetSoC.Value())); + } + + if (chargingTarget.addedEnergy.HasValue()) + { + ReturnErrorOnFailure(writer.Put(TLV::ContextTag(TargetEntryTag::kAddedEnergy), chargingTarget.addedEnergy.Value())); + } + + ReturnErrorOnFailure(writer.EndContainer(chargingTargetsStructType)); + } + ReturnErrorOnFailure(writer.EndContainer(chargingTargetsListType)); + ReturnErrorOnFailure(writer.EndContainer(evseTargetEntryType)); + } + + ReturnErrorOnFailure(writer.EndContainer(arrayType)); + + uint64_t len = static_cast(writer.GetLengthWritten()); + ChipLogProgress(AppServer, "SaveTargets: length written 0x" ChipLogFormatX64, ChipLogValueX64(len)); + + writer.Finalize(backingBuffer); + + ReturnErrorOnFailure(mpTargetStore->SyncSetKeyValue(spEvseTargetsKeyName, backingBuffer.Get(), static_cast(len))); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR EvseTargetsDelegate::ClearTargets() +{ + /* We simply delete the data from the persistent store */ + mpTargetStore->SyncDeleteKeyValue(spEvseTargetsKeyName); + + // Now reload from persistent storage so that mChargingTargetSchedulesList gets updated (it will be empty) + CHIP_ERROR err = LoadTargets(); + + return err; +} + +void EvseTargetsDelegate::PrintTargets( + const DataModel::List & chargingTargetSchedules) +{ + ChipLogProgress(AppServer, "---------------------- TARGETS ---------------------"); + + uint16_t chargingTargetScheduleIdx = 0; + for (auto & chargingTargetSchedule : chargingTargetSchedules) + { + [[maybe_unused]] uint8_t bitmask = + chargingTargetSchedule.dayOfWeekForSequence.GetField(static_cast(kAllTargetDaysMask)); + ChipLogProgress(AppServer, "idx %u dayOfWeekForSequence 0x%02x", chargingTargetScheduleIdx, bitmask); + + uint16_t chargingTargetIdx = 0; + for (auto & chargingTarget : chargingTargetSchedule.chargingTargets) + { + [[maybe_unused]] int64_t addedEnergy = chargingTarget.addedEnergy.HasValue() ? chargingTarget.addedEnergy.Value() : 0; + + ChipLogProgress( + AppServer, "chargingTargetIdx %u targetTimeMinutesPastMidnight %u targetSoC %u addedEnergy 0x" ChipLogFormatX64, + chargingTargetIdx, chargingTarget.targetTimeMinutesPastMidnight, + chargingTarget.targetSoC.HasValue() ? chargingTarget.targetSoC.Value() : 0, ChipLogValueX64(addedEnergy)); + + chargingTargetIdx++; + } + + chargingTargetScheduleIdx++; + } +} + +/** + * Part of the FabricTable::Delegate interface. Gets called when a fabric is deleted, such as on FabricTable::Delete(). + **/ +void EvseTargetsDelegate::OnFabricRemoved(const FabricTable & fabricTable, FabricIndex fabricIndex) {} diff --git a/examples/energy-management-app/energy-management-common/tests/BUILD.gn b/examples/energy-management-app/energy-management-common/tests/BUILD.gn new file mode 100644 index 00000000000000..b55340a527233d --- /dev/null +++ b/examples/energy-management-app/energy-management-common/tests/BUILD.gn @@ -0,0 +1,44 @@ +# Copyright (c) 2020-2024 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import("//build_overrides/build.gni") +import("//build_overrides/chip.gni") + +import("${chip_root}/build/chip/chip_test_suite.gni") + +config("tests_config") { + include_dirs = [ "${chip_root}/examples/energy-management-app/energy-management-common/include" ] +} + +chip_test_suite("tests") { + output_name = "libEnergyTest" + output_dir = "${root_out_dir}/lib" + + public_configs = [ ":tests_config" ] + + test_sources = [ "TestEvseTargetsStorage.cpp" ] + + cflags = [ "-Wconversion" ] + + public_deps = [ + "${chip_root}/examples/energy-management-app/energy-management-common", + "${chip_root}/examples/energy-management-app/linux:test-evse-targets-store", + "${chip_root}/src/app", + "${chip_root}/src/app/common:cluster-objects", + "${chip_root}/src/app/tests:helpers", + "${chip_root}/src/lib", + "${chip_root}/src/lib/core", + "${chip_root}/src/lib/support:testing", + ] +} diff --git a/examples/energy-management-app/energy-management-common/tests/TestEvseTargetsStorage.cpp b/examples/energy-management-app/energy-management-common/tests/TestEvseTargetsStorage.cpp new file mode 100644 index 00000000000000..6a1f2ec4c424f1 --- /dev/null +++ b/examples/energy-management-app/energy-management-common/tests/TestEvseTargetsStorage.cpp @@ -0,0 +1,219 @@ +/* + * + * Copyright (c) 2022 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "EnergyEvseTargetsStore.h" + +using namespace chip; +using namespace chip::app; +using namespace chip::app::DataModel; +using namespace chip::app::Clusters; +using namespace chip::app::Clusters::EnergyEvse; + +namespace { + +constexpr uint16_t ENERGY_EVSE_SET_TARGETS_DAYS_IN_A_WEEK = 7; +constexpr uint16_t ENERGY_EVSE_SET_TARGETS_MAX_CHARGING_TARGETS = 10; + +class TestEvseTargetsStorage : public ::testing::Test +{ +public: + static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); } + static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); } + + bool CompTargets(const DataModel::List & targets1, + const DataModel::List & targets2); + + void PopulateTargets(uint16_t numDays, uint16_t numChargingTargetsPerDay); + + void SetTargets(); + void CheckTargets(); + +private: + uint8_t mStore[4096]; + + EnergyEvse::Structs::ChargingTargetScheduleStruct::Type mChargingTargetSchedules[ENERGY_EVSE_SET_TARGETS_DAYS_IN_A_WEEK]; + EnergyEvse::Structs::ChargingTargetStruct::Type mChargingTargets[ENERGY_EVSE_SET_TARGETS_DAYS_IN_A_WEEK] + [ENERGY_EVSE_SET_TARGETS_MAX_CHARGING_TARGETS]; + chip::app::DataModel::List + mChargingTargetsList[ENERGY_EVSE_SET_TARGETS_DAYS_IN_A_WEEK]; + + chip::app::DataModel::List mRefChargingTargetSchedulesList; + DataModel::DecodableList mDecodableChargingTargetSchedulesList; + + TestPersistentStorageDelegate mStorageDelegate; + + EvseTargetsDelegate mEtd; + bool mEtdInitialised = false; +}; + +TEST_F(TestEvseTargetsStorage, TestEmpty) +{ + PopulateTargets(0, 0); + SetTargets(); + CheckTargets(); +} + +TEST_F(TestEvseTargetsStorage, TestFull) +{ + PopulateTargets(ENERGY_EVSE_SET_TARGETS_DAYS_IN_A_WEEK, ENERGY_EVSE_SET_TARGETS_MAX_CHARGING_TARGETS); + SetTargets(); + CheckTargets(); +} + +TEST_F(TestEvseTargetsStorage, TestPartial1) +{ + PopulateTargets(ENERGY_EVSE_SET_TARGETS_DAYS_IN_A_WEEK, 1); + SetTargets(); + CheckTargets(); +} + +TEST_F(TestEvseTargetsStorage, TestPartial2) +{ + PopulateTargets(1, ENERGY_EVSE_SET_TARGETS_MAX_CHARGING_TARGETS); + SetTargets(); + CheckTargets(); +} + +bool TestEvseTargetsStorage::CompTargets(const DataModel::List & targets1, + const DataModel::List & targets2) +{ + if (targets1.size() != targets2.size()) + { + ChipLogError(AppServer, "CompTargets: Different number of ChargingTargetScheduleStruct in lists"); + return false; + } + + uint16_t dayIdx = 0; + for (const auto & entry1 : targets1) + { + const auto & entry2 = targets2[dayIdx++]; + + if (entry1.dayOfWeekForSequence != entry2.dayOfWeekForSequence) + { + ChipLogError(AppServer, "CompTargets: Different dayOfWeekForSequence"); + return false; + } + + if (entry1.chargingTargets.size() != entry2.chargingTargets.size()) + { + ChipLogError(AppServer, "CompTargets: Different number of chargingTargets in day list"); + return false; + } + + uint16_t chargingTargetsIdx = 0; + for (const auto & targetStruct1 : entry1.chargingTargets) + { + const auto & targetStruct2 = entry2.chargingTargets[chargingTargetsIdx++]; + + if (targetStruct1.targetTimeMinutesPastMidnight != targetStruct2.targetTimeMinutesPastMidnight) + { + ChipLogError(AppServer, "CompTargets: Different targetTimeMinutesPastMidnight"); + return false; + } + + if (targetStruct1.targetSoC != targetStruct2.targetSoC) + { + ChipLogError(AppServer, "CompTargets: Different targetSoC"); + return false; + } + + if (targetStruct1.addedEnergy != targetStruct2.addedEnergy) + { + ChipLogError(AppServer, "CompTargets: Different addedEnergy"); + return false; + } + } + } + + return true; +} + +void TestEvseTargetsStorage::PopulateTargets(uint16_t numDays, uint16_t numChargingTargetsPerDay) +{ + for (uint16_t dayIdx = 0; dayIdx < numDays; dayIdx++) + { + for (uint16_t chargingTargetIdx = 0; chargingTargetIdx < numChargingTargetsPerDay; chargingTargetIdx++) + { + mChargingTargets[dayIdx][chargingTargetIdx].targetTimeMinutesPastMidnight = + static_cast(dayIdx * 60 + chargingTargetIdx); + mChargingTargets[dayIdx][chargingTargetIdx].targetSoC.SetValue(65); + mChargingTargets[dayIdx][chargingTargetIdx].addedEnergy.SetValue(400); + } + + mChargingTargetsList[dayIdx] = chip::app::DataModel::List( + mChargingTargets[dayIdx], numChargingTargetsPerDay); + + mChargingTargetSchedules[dayIdx].dayOfWeekForSequence.Set(static_cast(1 << dayIdx)); + mChargingTargetSchedules[dayIdx].chargingTargets = mChargingTargetsList[dayIdx]; + } + + chip::app::DataModel::List chargingTargetSchedulesList( + mChargingTargetSchedules, numDays); + + mRefChargingTargetSchedulesList = chargingTargetSchedulesList; + + TLV::TLVReader mReader; + TLV::TLVWriter mWriter; + + mWriter.Init(mStore, sizeof(mStore)); + + CHIP_ERROR err = DataModel::Encode(mWriter, TLV::AnonymousTag(), mRefChargingTargetSchedulesList); + EXPECT_EQ(err, CHIP_NO_ERROR); + + EXPECT_EQ(mWriter.Finalize(), CHIP_NO_ERROR); + + mReader.Init(mStore); + EXPECT_EQ(mReader.Next(), CHIP_NO_ERROR); + + err = DataModel::Decode(mReader, mDecodableChargingTargetSchedulesList); + EXPECT_EQ(err, CHIP_NO_ERROR); +} + +void TestEvseTargetsStorage::SetTargets() +{ + if (!mEtdInitialised) + { + mEtd.Init(&mStorageDelegate); + + mEtdInitialised = true; + } + + CHIP_ERROR err = mEtd.SetTargets(mDecodableChargingTargetSchedulesList); + EXPECT_EQ(err, CHIP_NO_ERROR); +} + +void TestEvseTargetsStorage::CheckTargets() +{ + const DataModel::List targets = mEtd.GetTargets(); + + EXPECT_TRUE(CompTargets(mRefChargingTargetSchedulesList, targets)); +} + +} // namespace diff --git a/examples/energy-management-app/linux/BUILD.gn b/examples/energy-management-app/linux/BUILD.gn index c3564c345a1182..1a6e2d287fed2e 100644 --- a/examples/energy-management-app/linux/BUILD.gn +++ b/examples/energy-management-app/linux/BUILD.gn @@ -18,8 +18,6 @@ import("${chip_root}/build/chip/tools.gni") import("${chip_root}/src/app/common_flags.gni") import("${chip_root}/third_party/imgui/imgui.gni") -assert(chip_build_tools) - import("${chip_root}/examples/common/pigweed/pigweed_rpcs.gni") if (chip_enable_pw_rpc) { @@ -36,6 +34,7 @@ config("includes") { executable("chip-energy-management-app") { sources = [ + "${chip_root}/examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DEMTestEventTriggers.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementManager.cpp", @@ -45,6 +44,7 @@ executable("chip-energy-management-app") { "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseEventTriggers.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseMain.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseTargetsStore.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyReportingEventTriggers.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyTimeUtils.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/FakeReadings.cpp", @@ -127,6 +127,23 @@ executable("chip-energy-management-app") { output_dir = root_out_dir } +source_set("test-evse-targets-store") { + sources = [ + "${chip_root}/examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseTargetsStore.cpp", + ] + + include_dirs = [ + "include", + "${chip_root}/examples/energy-management-app/energy-management-common/include", + ] + + deps = [ + "${chip_root}/examples/energy-management-app/energy-management-common", + "${chip_root}/src/lib", + ] +} + group("linux") { deps = [ ":chip-energy-management-app" ] } diff --git a/examples/energy-management-app/linux/main.cpp b/examples/energy-management-app/linux/main.cpp index 2d056e56a9b149..cdc2c4ed53d844 100644 --- a/examples/energy-management-app/linux/main.cpp +++ b/examples/energy-management-app/linux/main.cpp @@ -39,7 +39,7 @@ constexpr uint16_t kOptionFeatureMap = 'f'; // Define the chip::ArgParser command line structures for extending the command line to support the // -f/--featureMap option static chip::ArgParser::OptionDef sFeatureMapOptionDefs[] = { - { "featureSet", chip::ArgParser::kArgumentRequired, kOptionFeatureMap }, { NULL } + { "featureSet", chip::ArgParser::kArgumentRequired, kOptionFeatureMap }, { nullptr } }; static chip::ArgParser::OptionSet sCmdLineOptions = { @@ -74,11 +74,11 @@ static uint32_t ParseNumber(const char * pString) uint32_t num = 0; if (strlen(pString) > 2 && pString[0] == '0' && pString[1] == 'x') { - num = (uint32_t) strtoul(&pString[2], NULL, 16); + num = (uint32_t) strtoul(&pString[2], nullptr, 16); } else { - num = (uint32_t) strtoul(pString, NULL, 10); + num = (uint32_t) strtoul(pString, nullptr, 10); } return num; diff --git a/examples/platform/linux/BUILD.gn b/examples/platform/linux/BUILD.gn index cd79d2b42e3273..f8a5334d78c47d 100644 --- a/examples/platform/linux/BUILD.gn +++ b/examples/platform/linux/BUILD.gn @@ -84,6 +84,8 @@ source_set("app-main") { ":energy-evse-test-event-trigger", ":energy-reporting-test-event-trigger", ":smco-test-event-trigger", + "${chip_root}/src/controller:controller", + "${chip_root}/src/controller:gen_check_chip_controller_headers", "${chip_root}/src/lib", "${chip_root}/src/platform/logging:stdio", ] @@ -140,7 +142,11 @@ source_set("commissioner-main") { defines += [ "ENABLE_CHIP_SHELL" ] } - public_deps = [ "${chip_root}/src/lib" ] + public_deps = [ + "${chip_root}/src/controller:controller", + "${chip_root}/src/controller:gen_check_chip_controller_headers", + "${chip_root}/src/lib", + ] deps = [ "${chip_root}/src/app/server" ] if (chip_enable_transport_trace) { diff --git a/examples/shell/shell_common/BUILD.gn b/examples/shell/shell_common/BUILD.gn index 0e318ccb31a5ca..470f81b5d2459a 100644 --- a/examples/shell/shell_common/BUILD.gn +++ b/examples/shell/shell_common/BUILD.gn @@ -70,12 +70,14 @@ static_library("shell_common") { "${chip_root}/examples/all-clusters-app/all-clusters-common/src/smco-stub.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/static-supported-modes-manager.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/static-supported-temperature-levels.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/ChargingTargetsMemMgr.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/DeviceEnergyManagementManager.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/ElectricalPowerMeasurementDelegate.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp", + "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyEvseTargetsStore.cpp", "${chip_root}/examples/energy-management-app/energy-management-common/src/EnergyTimeUtils.cpp", ] diff --git a/src/BUILD.gn b/src/BUILD.gn index 7e5fedcc0d539b..c162a61f0620fa 100644 --- a/src/BUILD.gn +++ b/src/BUILD.gn @@ -155,6 +155,15 @@ if (chip_build_tests) { } } + chip_test_group("example_tests") { + deps = [] + tests = [] + if (chip_device_platform != "esp32" && chip_device_platform != "efr32" && + current_os != "android") { + tests += [ "${chip_root}/examples/energy-management-app/energy-management-common/tests" ] + } + } + chip_test_group("fake_platform_tests") { tests = [ "${chip_root}/src/lib/dnssd/platform/tests" ] } diff --git a/src/app/clusters/electrical-energy-measurement-server/electrical-energy-measurement-server.cpp b/src/app/clusters/electrical-energy-measurement-server/electrical-energy-measurement-server.cpp index f5c8c281615435..cdf480b64cdde4 100644 --- a/src/app/clusters/electrical-energy-measurement-server/electrical-energy-measurement-server.cpp +++ b/src/app/clusters/electrical-energy-measurement-server/electrical-energy-measurement-server.cpp @@ -25,6 +25,7 @@ #include #include #include +#include using chip::Protocols::InteractionModel::Status; diff --git a/src/app/clusters/electrical-power-measurement-server/electrical-power-measurement-server.h b/src/app/clusters/electrical-power-measurement-server/electrical-power-measurement-server.h index 8204a271434e63..53c741d9378cc4 100644 --- a/src/app/clusters/electrical-power-measurement-server/electrical-power-measurement-server.h +++ b/src/app/clusters/electrical-power-measurement-server/electrical-power-measurement-server.h @@ -21,7 +21,6 @@ #include #include -#include #include namespace chip { @@ -36,8 +35,6 @@ class Delegate void SetEndpointId(EndpointId aEndpoint) { mEndpointId = aEndpoint; } - using HarmonicMeasurementIterator = CommonIterator; - virtual PowerModeEnum GetPowerMode() = 0; virtual uint8_t GetNumberOfMeasurementTypes() = 0; diff --git a/src/app/clusters/energy-evse-server/EnergyEvseTestEventTriggerHandler.h b/src/app/clusters/energy-evse-server/EnergyEvseTestEventTriggerHandler.h index 307156e21f1197..38db739bc97f32 100644 --- a/src/app/clusters/energy-evse-server/EnergyEvseTestEventTriggerHandler.h +++ b/src/app/clusters/energy-evse-server/EnergyEvseTestEventTriggerHandler.h @@ -57,6 +57,8 @@ enum class EnergyEvseTrigger : uint64_t kEVChargeDemand = 0x0099000000000004, // EV Charge Demand Test Event Clear | Simulate the EV becoming fully charged kEVChargeDemandClear = 0x0099000000000005, + // EV Charge TimeOfUse Mode | Simulate putting the EVSE into a Mode with the TimeOfUse tag included + kEVTimeOfUseMode = 0x0099000000000006, // EVSE has a GroundFault fault kEVSEGroundFault = 0x0099000000000010, // EVSE has a OverTemperature fault @@ -65,6 +67,8 @@ enum class EnergyEvseTrigger : uint64_t kEVSEFaultClear = 0x0099000000000012, // EVSE Diagnostics Complete | Simulate diagnostics have been completed and return to normal kEVSEDiagnosticsComplete = 0x0099000000000020, + // EV Charge TimeOfUse Mode clear | Simulate clearing the EVSE Mode TimeOfUse tag + kEVTimeOfUseModeClear = 0x0099000000000021, }; class EnergyEvseTestEventTriggerHandler : public TestEventTriggerHandler diff --git a/src/app/clusters/energy-evse-server/energy-evse-server.cpp b/src/app/clusters/energy-evse-server/energy-evse-server.cpp index af825217e2565a..4807bf33969681 100644 --- a/src/app/clusters/energy-evse-server/energy-evse-server.cpp +++ b/src/app/clusters/energy-evse-server/energy-evse-server.cpp @@ -309,13 +309,13 @@ void Instance::HandleEnableCharging(HandlerContext & ctx, const Commands::Enable auto & minimumChargeCurrent = commandData.minimumChargeCurrent; auto & maximumChargeCurrent = commandData.maximumChargeCurrent; - if ((minimumChargeCurrent < kMinimumChargeCurrent) || (minimumChargeCurrent > kMaximumChargeCurrent)) + if (minimumChargeCurrent < kMinimumChargeCurrent) { ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } - if ((maximumChargeCurrent < kMinimumChargeCurrent) || (maximumChargeCurrent > kMaximumChargeCurrent)) + if (maximumChargeCurrent < kMinimumChargeCurrent) { ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; @@ -339,7 +339,7 @@ void Instance::HandleEnableDischarging(HandlerContext & ctx, const Commands::Ena auto & dischargingEnabledUntil = commandData.dischargingEnabledUntil; auto & maximumDischargeCurrent = commandData.maximumDischargeCurrent; - if ((maximumDischargeCurrent < kMinimumChargeCurrent) || (maximumDischargeCurrent > kMaximumChargeCurrent)) + if (maximumDischargeCurrent < kMinimumChargeCurrent) { ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; @@ -362,27 +362,136 @@ void Instance::HandleStartDiagnostics(HandlerContext & ctx, const Commands::Star void Instance::HandleSetTargets(HandlerContext & ctx, const Commands::SetTargets::DecodableType & commandData) { // Call the delegate - // TODO - // Status status = mDelegate.SetTargets(); - Status status = Status::UnsupportedCommand; + auto & chargingTargetSchedules = commandData.chargingTargetSchedules; + + Status status = ValidateTargets(chargingTargetSchedules); + if (status != Status::Success) + { + ChipLogError(AppServer, "SetTargets contained invalid data - Rejecting"); + } + else + { + status = mDelegate.SetTargets(chargingTargetSchedules); + } ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); } + +Status Instance::ValidateTargets( + const DataModel::DecodableList & chargingTargetSchedules) +{ + /* A) check that the targets are valid + * 1) each target must be within valid range (TargetTimeMinutesPastMidnight < 1440) + * 2) each target must be within valid range (TargetSoC percent 0 - 100) + * If SOC feature not supported then this MUST be 100 or not present + * 3) each target must be within valid range (AddedEnergy >= 0) + * B) Day of Week is only allowed to be included once + */ + + uint8_t dayOfWeekBitmap = 0; + + auto iter = chargingTargetSchedules.begin(); + while (iter.Next()) + { + auto & entry = iter.GetValue(); + uint8_t bitmask = entry.dayOfWeekForSequence.GetField(static_cast(0x7F)); + ChipLogProgress(AppServer, "DayOfWeekForSequence = 0x%02x", bitmask); + + if ((dayOfWeekBitmap & bitmask) != 0) + { + // A bit has already been set - Return ConstraintError + ChipLogError(AppServer, "DayOfWeekForSequence has a bit set which has already been set in another entry."); + return Status::ConstraintError; + } + dayOfWeekBitmap |= bitmask; // add this day Of week to the previously seen days + + auto iterInner = entry.chargingTargets.begin(); + uint8_t innerIdx = 0; + while (iterInner.Next()) + { + auto & targetStruct = iterInner.GetValue(); + uint16_t minutesPastMidnight = targetStruct.targetTimeMinutesPastMidnight; + ChipLogProgress(AppServer, "[%d] MinutesPastMidnight : %d", innerIdx, + static_cast(minutesPastMidnight)); + + if (minutesPastMidnight > 1439) + { + ChipLogError(AppServer, "MinutesPastMidnight has invalid value (%d)", static_cast(minutesPastMidnight)); + return Status::ConstraintError; + } + + // If SocReporting is supported, targetSoc must have a value in the range [0, 100] + if (HasFeature(Feature::kSoCReporting)) + { + if (!targetStruct.targetSoC.HasValue()) + { + ChipLogError(AppServer, "kSoCReporting is supported but TargetSoC does not have a value"); + return Status::Failure; + } + + if (targetStruct.targetSoC.Value() > 100) + { + ChipLogError(AppServer, "TargetSoC has invalid value (%d)", static_cast(targetStruct.targetSoC.Value())); + return Status::ConstraintError; + } + } + else if (targetStruct.targetSoC.HasValue() && targetStruct.targetSoC.Value() != 100) + { + // If SocReporting is not supported but targetSoc has a value, it must be 100 + ChipLogError(AppServer, "TargetSoC has can only be 100%% if SOC feature is not supported"); + return Status::ConstraintError; + } + + // One or both of targetSoc and addedEnergy must be specified + if (!(targetStruct.targetSoC.HasValue()) && !(targetStruct.addedEnergy.HasValue())) + { + ChipLogError(AppServer, "Must have one of AddedEnergy or TargetSoC"); + return Status::Failure; + } + + // Validate the value of addedEnergy, if specified is >= 0 + if (targetStruct.addedEnergy.HasValue() && targetStruct.addedEnergy.Value() < 0) + { + ChipLogError(AppServer, "AddedEnergy has invalid value (%ld)", + static_cast(targetStruct.addedEnergy.Value())); + return Status::ConstraintError; + } + innerIdx++; + } + + if (iterInner.GetStatus() != CHIP_NO_ERROR) + { + return Status::InvalidCommand; + } + } + + if (iter.GetStatus() != CHIP_NO_ERROR) + { + return Status::InvalidCommand; + } + + return Status::Success; +} + void Instance::HandleGetTargets(HandlerContext & ctx, const Commands::GetTargets::DecodableType & commandData) { - // Call the delegate - // TODO - // Status status = mDelegate.GetTargets(); - Status status = Status::UnsupportedCommand; + Commands::GetTargetsResponse::Type response; - ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); + Status status = mDelegate.GetTargets(response.chargingTargetSchedules); + if (status != Status::Success) + { + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); + return; + } + + ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Protocols::InteractionModel::Status::Success); } + void Instance::HandleClearTargets(HandlerContext & ctx, const Commands::ClearTargets::DecodableType & commandData) { // Call the delegate - // TODO - // Status status = mDelegate.ClearTargets(); - Status status = Status::UnsupportedCommand; + Status status = mDelegate.ClearTargets(); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); } diff --git a/src/app/clusters/energy-evse-server/energy-evse-server.h b/src/app/clusters/energy-evse-server/energy-evse-server.h index 2ff46f339ff6ad..979e44c040a17b 100644 --- a/src/app/clusters/energy-evse-server/energy-evse-server.h +++ b/src/app/clusters/energy-evse-server/energy-evse-server.h @@ -36,8 +36,9 @@ namespace EnergyEvse { // Spec-defined constraints constexpr int64_t kMinimumChargeCurrent = 0; -constexpr int64_t kMaximumChargeCurrent = 80000; constexpr uint32_t kMaxRandomizationDelayWindow = 86400; +constexpr uint8_t kEvseTargetsMaxNumberOfDays = 7; +constexpr uint8_t kEvseTargetsMaxTargetsPerDay = 10; /** @brief * Defines methods for implementing application-specific logic for the EVSE Management Cluster. @@ -81,6 +82,36 @@ class Delegate */ virtual Protocols::InteractionModel::Status StartDiagnostics() = 0; + /** + * @brief Delegate should implement a handler for the SetTargets command. + * It should report Status::Success if successful and may + * return other Status codes if it fails + */ + virtual Protocols::InteractionModel::Status + SetTargets(const DataModel::DecodableList & chargingTargetSchedules) = 0; + + /** + * @brief Delegate should implement a handler for LoadTargets + * + * This needs to load any stored targets into memory + */ + virtual Protocols::InteractionModel::Status LoadTargets() = 0; + + /** + * @brief Delegate should implement a handler for GetTargets + * + * @param[out] The full targets structure + */ + virtual Protocols::InteractionModel::Status + GetTargets(DataModel::List & chargingTargetSchedules) = 0; + + /** + * @brief Delegate should implement a handler for ClearTargets command. + * It should report Status::Success if successful and may + * return other Status codes if it fails + */ + virtual Protocols::InteractionModel::Status ClearTargets() = 0; + // ------------------------------------------------------------------ // Get attribute methods virtual StateEnum GetState() = 0; @@ -178,6 +209,10 @@ class Instance : public AttributeAccessInterface, public CommandHandlerInterface void HandleSetTargets(HandlerContext & ctx, const Commands::SetTargets::DecodableType & commandData); void HandleGetTargets(HandlerContext & ctx, const Commands::GetTargets::DecodableType & commandData); void HandleClearTargets(HandlerContext & ctx, const Commands::ClearTargets::DecodableType & commandData); + + // Check that the targets are valid + Protocols::InteractionModel::Status + ValidateTargets(const DataModel::DecodableList & chargingTargetSchedules); }; } // namespace EnergyEvse diff --git a/src/python_testing/TC_EEVSE_2_3.py b/src/python_testing/TC_EEVSE_2_3.py new file mode 100644 index 00000000000000..d8d8f8fdfb96b5 --- /dev/null +++ b/src/python_testing/TC_EEVSE_2_3.py @@ -0,0 +1,454 @@ +# +# Copyright (c) 2024 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments +# for details about the block below. +# +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: run1 +# test-runner-run/run1/app: ${ENERGY_MANAGEMENT_APP} +# test-runner-run/run1/factoryreset: True +# test-runner-run/run1/quiet: True +# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json --enable-key 000102030405060708090a0b0c0d0e0f +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --hex-arg enableKey:000102030405060708090a0b0c0d0e0f --endpoint 1 --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto +# === END CI TEST ARGUMENTS === + +import logging +from datetime import datetime, timedelta, timezone + +import chip.clusters as Clusters +from chip.clusters.Types import NullValue +from chip.interaction_model import Status +from matter_testing_support import EventChangeCallback, MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from mobly import asserts +from TC_EEVSE_Utils import EEVSEBaseTestHelper + +logger = logging.getLogger(__name__) + + +class TC_EEVSE_2_3(MatterBaseTest, EEVSEBaseTestHelper): + + def desc_TC_EEVSE_2_3(self) -> str: + """Returns a description of this test""" + return "5.1.4. [TC-EEVSE-2.3] Optional ChargingPreferences feature functionality with DUT as Server\n" \ + "This test case verifies the primary functionality of the Energy EVSE cluster server with the optional ChargingPreferences feature supported." + + def pics_TC_EEVSE_2_3(self): + """ This function returns a list of PICS for this test case that must be True for the test to be run""" + return ["EEVSE.S", "EEVSE.S.F00"] + + def steps_TC_EEVSE_2_3(self) -> list[TestStep]: + steps = [ + TestStep("1", "Commissioning, already done", + is_commissioning=True), + TestStep("2", "TH reads TestEventTriggersEnabled attribute from General Diagnostics Cluster.", + "Verify value is 1 (True)"), + TestStep("3", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for Basic Functionality Test Event.", + "Verify Command response is Success"), + TestStep("4", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EVSE TimeOfUse Mode Test Event.", + "Verify Command response is Success"), + TestStep("5", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Plugged-in Test Event.", + "Verify Command response is Success and event EEVSE.S.E00(EVConnected) sent"), + TestStep("6", "TH sends ClearTargets.", + "Verify Command response is Success"), + TestStep("6a", "TH reads NextChargeStartTime attribute.", + "Verify value is null."), + TestStep("6b", "TH reads NextChargeTargetTime attribute.", + "Verify value is null."), + TestStep("6c", "TH reads NextChargeRequiredEnergy attribute.", + "Verify value is null."), + TestStep("6d", "TH reads NextChargeTargetSoC attribute.", + "Verify value is null."), + TestStep("7", "TH sends GetTargets.", + "Response EEVSE.S.C00.Tx(GetTargetsResponse) sent with no targets defined."), + TestStep("8", "TH sends SetTargets with DayOfTheWeekforSequence=0x7F (i.e. having all days set) and a single ChargingTargets={TargetTime=1439, TargetSoC=null, AddedEnergy=25000000}.", + "Verify Command response is Success"), + TestStep("8a", "TH reads NextChargeStartTime attribute.", + "Verify value is null."), + TestStep("8b", "TH reads NextChargeTargetTime attribute.", + "Verify value is null."), + TestStep("8c", "TH reads NextChargeRequiredEnergy attribute.", + "Verify value is null."), + TestStep("8d", "TH reads NextChargeTargetSoC attribute.", + "Verify value is null."), + TestStep("9", "TH sends EnableCharging with ChargingEnabledUntil=null, minimumChargeCurrent=6000, maximumChargeCurrent=60000.", + "Verify Command response is Success"), + TestStep("9a", "TH reads NextChargeStartTime attribute.", + "Verify value is before the next TargetTime above."), + TestStep("9b", "TH reads NextChargeTargetTime attribute.", + "Verify value is TargetTime above."), + TestStep("9c", "TH reads NextChargeRequiredEnergy attribute.", + "Verify value is AddedEnergy above."), + TestStep("9d", "TH reads NextChargeTargetSoC attribute.", + "Verify value is null."), + TestStep("10", "TH sends GetTargets.", + "Response EEVSE.S.C00.Tx(GetTargetsResponse) sent with targets equivalent to the above (Note 1)."), + TestStep("11", "TH sends SetTargets with DayOfTheWeekforSequence=0x7F (i.e. having all days set) and a single ChargingTargets={TargetTime=1, TargetSoC=100, AddedEnergy=null}.", + "Verify Command response is Success"), + TestStep("11a", "TH reads NextChargeStartTime attribute.", + "Verify value is before the next TargetTime above."), + TestStep("11b", "TH reads NextChargeTargetTime attribute.", + "Verify value is TargetTime above."), + TestStep("11c", "TH reads NextChargeRequiredEnergy attribute.", + "Verify value is null."), + TestStep("11d", "TH reads NextChargeTargetSoC attribute.", + "Verify value is 100."), + TestStep("12", "TH sends GetTargets.", + "Response EEVSE.S.C00.Tx(GetTargetsResponse) sent with targets equivalent to the above (Note 1)."), + TestStep("13", "TH sends SetTargets with DayOfTheWeekforSequence=0x40 (i.e. having Saturday set) and 10 ChargingTargets with TargetTimes=60,180,300,420,540,660,780,900,1020,1140 and all with TargetSoC=null, AddedEnergy=2500000.", + "Verify Command response is Success"), + TestStep("14", "TH sends SetTargets with DayOfTheWeekforSequence=0x01 (i.e. having Sunday set) and no ChargingTargets.", + "Verify Command response is Success"), + TestStep("15", "TH sends GetTargets.", + "Response EEVSE.S.C00.Tx(GetTargetsResponse) sent with 1 target for each day Monday to Friday equivalent to step 9 (Note 1), 10 targets for Saturday as step 11, and no targets for Sunday."), + TestStep("16", "TH sends ClearTargets.", + "Verify Command response is Success"), + TestStep("16a", "TH reads NextChargeStartTime attribute.", + "Verify value is null."), + TestStep("16b", "TH reads NextChargeTargetTime attribute.", + "Verify value is null."), + TestStep("16c", "TH reads NextChargeRequiredEnergy attribute.", + "Verify value is null."), + TestStep("16d", "TH reads NextChargeTargetSoC attribute.", + "Verify value is null."), + TestStep("17", "TH sends GetTargets.", + "Response EEVSE.S.C00.Tx(GetTargetsResponse) sent with no targets defined."), + TestStep("18", "TH sends SetTargets with two identical ChargingTargetSchedules={DayOfTheWeekforSequence=0x01,ChargingTarget[0]={TargetTime=60,TargetSoC=null,AddedEnergy=2500000}}.", + "Verify Command response is ConstraintError"), + TestStep("19", "TH sends SetTargets with DayOfTheWeekforSequence=0x40 and 11 ChargingTargets with TargetTimes=60,180,300,420,540,660,780,900,1020,1140,1260 and all with TargetSoC=null, AddedEnergy=2500000.", + "Verify Command response is ResourceExhausted"), + TestStep("20", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Plugged-in Test Event Clear.", + "Verify Command response is Success and event EEVSE.S.E01(EVNotDetected) sent"), + TestStep("21", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for Basic Functionality Test Event Clear.", + "Verify Command response is Success"), + ] + + return steps + + def log_get_targets_response(self, get_targets_response): + logger.info(f" Rx'd: {get_targets_response}") + for index, entry in enumerate(get_targets_response.chargingTargetSchedules): + logger.info( + f" [{index}] DayOfWeekForSequence: {entry.dayOfWeekForSequence:02x}") + for sub_index, sub_entry in enumerate(entry.chargingTargets): + logger.info( + f" - [{sub_index}] TargetTime: {sub_entry.targetTimeMinutesPastMidnight} TargetSoC: {sub_entry.targetSoC} AddedEnergy: {sub_entry.addedEnergy}") + + def convert_epoch_s_to_time(self, epoch_s, tz=timezone.utc): + delta_from_epoch = timedelta(seconds=epoch_s) + matter_epoch = datetime(2000, 1, 1, 0, 0, 0, 0, tz) + + return matter_epoch + delta_from_epoch + + def compute_expected_target_time_as_epoch_s(self, minutes_past_midnight): + """Takes minutes past midnight and assumes local timezone, returns the value in Matter Epoch_S""" + # Matter epoch is 0 hours, 0 minutes, 0 seconds on Jan 1, 2000 UTC + # Get the current midnight + minutesPastMidnight as epoch_s + # NOTE that MinutesPastMidnight is in LOCAL time not UTC so it reflects + # the charging time based on where the consumer is. + target_time = datetime.now() # Get time in local time + target_time = target_time.replace(hour=int(minutes_past_midnight / 60), + minute=(minutes_past_midnight % 60), second=0, + microsecond=0) # Align to minutes past midnight + + if (target_time < datetime.now()): + # This is in the past - so we need to add 1 day + # We can get away with this in this test scenario - but should + # really look at the next target on this day to see if that is in the future + target_time = target_time + timedelta(days=1) + + # Shift to UTC so we can use timezone aware subtraction from Matter epoch in UTC + target_time = target_time.astimezone(timezone.utc) + + logger.info( + f"minutesPastMidnight = {minutes_past_midnight} => " + f"{int(minutes_past_midnight/60)}:{int(minutes_past_midnight%60)}" + f" Expected target_time = {target_time}") + + target_time_delta = target_time - \ + datetime(2000, 1, 1, 0, 0, 0, 0).astimezone(timezone.utc) + expected_target_time_epoch_s = int(target_time_delta.total_seconds()) + return expected_target_time_epoch_s + + @async_test_body + async def test_TC_EEVSE_2_3(self): + + self.step("1") + # Commission DUT - already done + + # Subscribe to Events and when they are sent push them to a queue for checking later + events_callback = EventChangeCallback(Clusters.EnergyEvse) + await events_callback.start(self.default_controller, + self.dut_node_id, + self.matter_test_config.endpoint) + + self.step("2") + await self.check_test_event_triggers_enabled() + + self.step("3") + await self.send_test_event_trigger_basic() + + self.step("4") + await self.send_test_event_trigger_time_of_use_mode() + + self.step("5") + await self.send_test_event_trigger_pluggedin() + event_data = events_callback.wait_for_event_report( + Clusters.EnergyEvse.Events.EVConnected) + session_id = event_data.sessionID + + self.step("6") + await self.send_clear_targets_command() + + self.step("6a") + await self.check_evse_attribute("NextChargeStartTime", NullValue) + + self.step("6b") + await self.check_evse_attribute("NextChargeTargetTime", NullValue) + + self.step("6c") + await self.check_evse_attribute("NextChargeRequiredEnergy", NullValue) + + self.step("6d") + await self.check_evse_attribute("NextChargeTargetSoC", NullValue) + + self.step("7") + get_targets_response = await self.send_get_targets_command() + self.log_get_targets_response(get_targets_response) + empty_targets_response = Clusters.EnergyEvse.Commands.GetTargetsResponse( + chargingTargetSchedules=[]) + asserts.assert_equal(get_targets_response, empty_targets_response, + f"Unexpected 'GetTargets' response value - expected {empty_targets_response}, was {get_targets_response}") + + self.step("8") + # The targets is a list of up to 7x ChargingTargetScheduleStruct's (one per day) + # each containing a list of up to 10x targets per day + minutes_past_midnight = 1439 + dailyTargets = [Clusters.EnergyEvse.Structs.ChargingTargetStruct(targetTimeMinutesPastMidnight=minutes_past_midnight, + # targetSoc not sent + addedEnergy=25000000)] + targets = [Clusters.EnergyEvse.Structs.ChargingTargetScheduleStruct( + dayOfWeekForSequence=0x7F, chargingTargets=dailyTargets)] + # This should be all days Sun-Sat (0x7F) with an TargetTime 1439 and added Energy 25kWh + await self.send_set_targets_command(chargingTargetSchedules=targets) + + self.step("8a") + await self.check_evse_attribute("NextChargeStartTime", NullValue) + + self.step("8b") + await self.check_evse_attribute("NextChargeTargetTime", NullValue) + + self.step("8c") + await self.check_evse_attribute("NextChargeRequiredEnergy", NullValue) + + self.step("8d") + await self.check_evse_attribute("NextChargeTargetSoC", NullValue) + + self.step("9") + await self.send_enable_charge_command(charge_until=NullValue, min_charge=6000, max_charge=60000) + + self.step("9a") + next_start_time_epoch_s = await self.read_evse_attribute_expect_success(attribute="NextChargeStartTime") + + expected_next_start_time_epoch_s = self.compute_expected_target_time_as_epoch_s( + minutes_past_midnight) + asserts.assert_less(next_start_time_epoch_s, + expected_next_start_time_epoch_s) + + self.step("9b") + await self.check_evse_attribute("NextChargeTargetTime", expected_next_start_time_epoch_s) + + self.step("9c") + await self.check_evse_attribute("NextChargeRequiredEnergy", 25000000) + + self.step("9d") + await self.check_evse_attribute("NextChargeTargetSoC", NullValue) + + self.step("10") + get_targets_response = await self.send_get_targets_command() + self.log_get_targets_response(get_targets_response) + asserts.assert_equal(get_targets_response.chargingTargetSchedules, targets, + f"Unexpected 'GetTargets' response value - expected {targets}, was {get_targets_response}") + + self.step("11") + # This should be all days Sun-Sat (0x7F) with an TargetTime 1 and SoC of 100%, AddedEnergy= NullValue + minutes_past_midnight = 1 + daily_targets_step_11 = [Clusters.EnergyEvse.Structs.ChargingTargetStruct(targetTimeMinutesPastMidnight=minutes_past_midnight, + targetSoC=100)] + targets_step_11 = [Clusters.EnergyEvse.Structs.ChargingTargetScheduleStruct( + dayOfWeekForSequence=0x7F, chargingTargets=daily_targets_step_11)] + + await self.send_set_targets_command(chargingTargetSchedules=targets_step_11) + + self.step("11a") + next_start_time_epoch_s = await self.read_evse_attribute_expect_success(attribute="NextChargeStartTime") + logger.info( + f"Received NextChargeStartTime: {next_start_time_epoch_s} = {self.convert_epoch_s_to_time(next_start_time_epoch_s, tz=None)}") + + self.step("11b") + next_target_time_epoch_s = await self.read_evse_attribute_expect_success(attribute="NextChargeTargetTime") + logger.info( + f"Received NextChargeTargetTime: {next_target_time_epoch_s} = {self.convert_epoch_s_to_time(next_target_time_epoch_s, tz=None)}") + + # This should be the next MinutesPastMidnight converted to realtime as epoch_s + expected_target_time_epoch_s = self.compute_expected_target_time_as_epoch_s( + minutes_past_midnight) + + asserts.assert_less(next_start_time_epoch_s, next_target_time_epoch_s, + f"Unexpected 'NextChargeStartTime' response value - expected this to be < {next_target_time_epoch_s}, was {next_start_time_epoch_s}") + + asserts.assert_equal(next_target_time_epoch_s, expected_target_time_epoch_s, + f"Unexpected 'NextChargeTargetTime' response value - expected {expected_target_time_epoch_s} = {self.convert_epoch_s_to_time(expected_target_time_epoch_s, tz=None)}, was {next_target_time_epoch_s} = {self.convert_epoch_s_to_time(next_target_time_epoch_s, tz=None)}") + + self.step("11c") + await self.check_evse_attribute("NextChargeRequiredEnergy", NullValue) + + self.step("11d") + await self.check_evse_attribute("NextChargeTargetSoC", 100) + + self.step("12") + get_targets_response = await self.send_get_targets_command() + self.log_get_targets_response(get_targets_response) + # This should be the same as targets_step_11 + asserts.assert_equal(get_targets_response.chargingTargetSchedules, targets_step_11, + f"Unexpected 'GetTargets' response value - expected {targets_step_11}, was {get_targets_response}") + + self.step("13") + # This should modify Sat (0x40) with 10 targets throughout the day + daily_targets_step_13 = [ + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=60, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=180, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=300, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=420, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=540, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=660, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=780, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=900, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=1020, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=1140, addedEnergy=25000000), + ] + targets_step_13 = [Clusters.EnergyEvse.Structs.ChargingTargetScheduleStruct( + dayOfWeekForSequence=0x40, chargingTargets=daily_targets_step_13)] + await self.send_set_targets_command(chargingTargetSchedules=targets_step_13) + + self.step("14") + # This should modify Sun (0x01) with NO targets on that day + daily_targets_step_14 = [] + targets_step_14 = [Clusters.EnergyEvse.Structs.ChargingTargetScheduleStruct( + dayOfWeekForSequence=0x01, chargingTargets=daily_targets_step_14)] + await self.send_set_targets_command(chargingTargetSchedules=targets_step_14) + + self.step("15") + get_targets_response = await self.send_get_targets_command() + self.log_get_targets_response(get_targets_response) + # We should expect that there should be 3 entries: + # [0] This should be all days (except Sun & Sat) = 0x3e with an TargetTime 1439 and added Energy 25kWh (from step 9) + # [1] This should be (Sat) = 0x40 with 10 TargetTimes and added Energy 25kWh (from step 11) + # [2] This should be (Sun) = 0x01 with NO Targets (from step 12) + # TODO - it would be better to iterate through each day and check it matches + asserts.assert_equal(len(get_targets_response.chargingTargetSchedules), 3, + "'GetTargets' response should have 3 entries") + asserts.assert_equal(get_targets_response.chargingTargetSchedules[0].dayOfWeekForSequence, 0x3e, + "'GetTargets' response entry 0 should have DayOfWeekForSequence = 0x3e (62)") + asserts.assert_equal(get_targets_response.chargingTargetSchedules[0].chargingTargets, daily_targets_step_11, + f"'GetTargets' response entry 0 should have chargingTargets = {daily_targets_step_11})") + asserts.assert_equal(get_targets_response.chargingTargetSchedules[1], targets_step_13[0], + f"'GetTargets' response entry 1 should be {targets_step_13})") + asserts.assert_equal(get_targets_response.chargingTargetSchedules[2], targets_step_14[0], + f"'GetTargets' response entry 2 should be {targets_step_14})") + + self.step("16") + await self.send_clear_targets_command() + + self.step("16a") + await self.check_evse_attribute("NextChargeStartTime", NullValue) + + self.step("16b") + await self.check_evse_attribute("NextChargeTargetTime", NullValue) + + self.step("16c") + await self.check_evse_attribute("NextChargeRequiredEnergy", NullValue) + + self.step("16d") + await self.check_evse_attribute("NextChargeTargetSoC", NullValue) + + self.step("17") + get_targets_response = await self.send_get_targets_command() + self.log_get_targets_response(get_targets_response) + asserts.assert_equal(get_targets_response, empty_targets_response, + f"Unexpected 'GetTargets' response value - expected {empty_targets_response}, was {get_targets_response}") + + self.step("18") + daily_targets_step_18 = [Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=60, addedEnergy=25000000)] + targets_step_18 = [Clusters.EnergyEvse.Structs.ChargingTargetScheduleStruct(dayOfWeekForSequence=0x1, chargingTargets=daily_targets_step_18), + Clusters.EnergyEvse.Structs.ChargingTargetScheduleStruct(dayOfWeekForSequence=0x1, chargingTargets=daily_targets_step_18)] + await self.send_set_targets_command(chargingTargetSchedules=targets_step_18, expected_status=Status.ConstraintError) + + self.step("19") + daily_targets_step_19 = [ + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=60, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=180, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=300, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=420, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=540, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=660, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=780, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=900, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=1020, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=1140, addedEnergy=25000000), + Clusters.EnergyEvse.Structs.ChargingTargetStruct( + targetTimeMinutesPastMidnight=1260, addedEnergy=25000000), + ] + + targets_step_19 = [Clusters.EnergyEvse.Structs.ChargingTargetScheduleStruct( + dayOfWeekForSequence=0x40, chargingTargets=daily_targets_step_19)] + await self.send_set_targets_command(chargingTargetSchedules=targets_step_19, expected_status=Status.ResourceExhausted) + + self.step("20") + await self.send_test_event_trigger_pluggedin_clear() + event_data = events_callback.wait_for_event_report( + Clusters.EnergyEvse.Events.EVNotDetected) + expected_state = Clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand + self.validate_ev_not_detected_event( + event_data, session_id, expected_state, expected_duration=0, expected_charged=0) + + self.step("21") + await self.send_test_event_trigger_basic_clear() + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_EEVSE_Utils.py b/src/python_testing/TC_EEVSE_Utils.py index 6b6b1395072ba0..17f64191ed37ec 100644 --- a/src/python_testing/TC_EEVSE_Utils.py +++ b/src/python_testing/TC_EEVSE_Utils.py @@ -16,8 +16,10 @@ import logging +import typing import chip.clusters as Clusters +from chip.clusters.Types import NullValue from chip.interaction_model import InteractionModelError, Status from mobly import asserts @@ -36,6 +38,22 @@ async def check_evse_attribute(self, attribute, expected_value, endpoint: int = asserts.assert_equal(value, expected_value, f"Unexpected '{attribute}' value - expected {expected_value}, was {value}") + def check_value_in_range(self, attribute: str, value: int, lower_value: int, upper_value: int): + asserts.assert_greater_equal(value, lower_value, + f"Unexpected '{attribute}' value - expected {lower_value}, was {value}") + asserts.assert_less_equal(value, upper_value, + f"Unexpected '{attribute}' value - expected {upper_value}, was {value}") + + async def check_evse_attribute_in_range(self, attribute, lower_value: int, upper_value: int, endpoint: int = None, allow_null: bool = False): + value = await self.read_evse_attribute_expect_success(endpoint=endpoint, attribute=attribute) + if allow_null and value is NullValue: + # skip the range check + logger.info("value is NULL - OK") + return value + + self.check_value_in_range(attribute, value, lower_value, upper_value) + return value + async def get_supported_energy_evse_attributes(self, endpoint: int = None): return await self.read_evse_attribute_expect_success(endpoint, "AttributeList") @@ -45,7 +63,8 @@ async def write_user_max_charge(self, endpoint: int = None, user_max_charge: int result = await self.default_controller.WriteAttribute(self.dut_node_id, [(endpoint, Clusters.EnergyEvse.Attributes.UserMaximumChargeCurrent(user_max_charge))]) - asserts.assert_equal(result[0].Status, Status.Success, "UserMaximumChargeCurrent write failed") + asserts.assert_equal( + result[0].Status, Status.Success, "UserMaximumChargeCurrent write failed") async def send_enable_charge_command(self, endpoint: int = None, charge_until: int = None, timedRequestTimeoutMs: int = 3000, min_charge: int = 6000, max_charge: int = 32000, expected_status: Status = Status.Success): @@ -58,7 +77,8 @@ async def send_enable_charge_command(self, endpoint: int = None, charge_until: i timedRequestTimeoutMs=timedRequestTimeoutMs) except InteractionModelError as e: - asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + asserts.assert_equal(e.status, expected_status, + "Unexpected error returned") async def send_disable_command(self, endpoint: int = None, timedRequestTimeoutMs: int = 3000, expected_status: Status = Status.Success): try: @@ -67,7 +87,8 @@ async def send_disable_command(self, endpoint: int = None, timedRequestTimeoutMs timedRequestTimeoutMs=timedRequestTimeoutMs) except InteractionModelError as e: - asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + asserts.assert_equal(e.status, expected_status, + "Unexpected error returned") async def send_start_diagnostics_command(self, endpoint: int = None, timedRequestTimeoutMs: int = 3000, expected_status: Status = Status.Success): @@ -77,7 +98,46 @@ async def send_start_diagnostics_command(self, endpoint: int = None, timedReques timedRequestTimeoutMs=timedRequestTimeoutMs) except InteractionModelError as e: - asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + asserts.assert_equal(e.status, expected_status, + "Unexpected error returned") + + async def send_clear_targets_command(self, endpoint: int = None, timedRequestTimeoutMs: int = 3000, + expected_status: Status = Status.Success): + try: + await self.send_single_cmd(cmd=Clusters.EnergyEvse.Commands.ClearTargets(), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, + "Unexpected error returned") + + async def send_get_targets_command(self, endpoint: int = None, timedRequestTimeoutMs: int = 3000, + expected_status: Status = Status.Success): + try: + get_targets_resp = await self.send_single_cmd(cmd=Clusters.EnergyEvse.Commands.GetTargets(), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, + "Unexpected error returned") + + return get_targets_resp + + async def send_set_targets_command(self, endpoint: int = None, + chargingTargetSchedules: typing.List[ + Clusters.EnergyEvse.Structs.ChargingTargetScheduleStruct] = None, + timedRequestTimeoutMs: int = 3000, + expected_status: Status = Status.Success): + try: + await self.send_single_cmd(cmd=Clusters.EnergyEvse.Commands.SetTargets(chargingTargetSchedules), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, + "Unexpected error returned") async def send_test_event_trigger_basic(self): await self.send_test_event_triggers(eventTrigger=0x0099000000000000) @@ -97,6 +157,9 @@ async def send_test_event_trigger_charge_demand(self): async def send_test_event_trigger_charge_demand_clear(self): await self.send_test_event_triggers(eventTrigger=0x0099000000000005) + async def send_test_event_trigger_time_of_use_mode(self): + await self.send_test_event_triggers(eventTrigger=0x0099000000000006) + async def send_test_event_trigger_evse_ground_fault(self): await self.send_test_event_triggers(eventTrigger=0x0099000000000010)