From 27bd1520e81ed736e85db5253cd41aed905016c3 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Sun, 27 Nov 2022 20:26:15 +0100 Subject: [PATCH] Add support for HomeWizard enable/disable cloud feature (#82573) --- homeassistant/components/homewizard/const.py | 3 +- .../components/homewizard/coordinator.py | 5 ++ .../components/homewizard/diagnostics.py | 3 + .../components/homewizard/manifest.json | 2 +- homeassistant/components/homewizard/switch.py | 51 +++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homewizard/conftest.py | 17 +++-- .../homewizard/fixtures/system.json | 3 + tests/components/homewizard/generator.py | 8 ++- .../components/homewizard/test_diagnostics.py | 1 + tests/components/homewizard/test_switch.py | 62 ++++++++++++++++++- 12 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 tests/components/homewizard/fixtures/system.json diff --git a/homeassistant/components/homewizard/const.py b/homeassistant/components/homewizard/const.py index 677f7ec46d9..f1aba6ca17a 100644 --- a/homeassistant/components/homewizard/const.py +++ b/homeassistant/components/homewizard/const.py @@ -5,7 +5,7 @@ from datetime import timedelta from typing import TypedDict # Set up. -from homewizard_energy.models import Data, Device, State +from homewizard_energy.models import Data, Device, State, System from homeassistant.const import Platform @@ -30,3 +30,4 @@ class DeviceResponseEntry(TypedDict): device: Device data: Data state: State + system: System diff --git a/homeassistant/components/homewizard/coordinator.py b/homeassistant/components/homewizard/coordinator.py index bab7b5d3ba3..4397d77a3ff 100644 --- a/homeassistant/components/homewizard/coordinator.py +++ b/homeassistant/components/homewizard/coordinator.py @@ -39,8 +39,13 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] "device": await self.api.device(), "data": await self.api.data(), "state": await self.api.state(), + "system": None, } + features = await self.api.features() + if features.has_system: + data["system"] = await self.api.system() + except RequestError as ex: raise UpdateFailed("Device did not respond as expected") from ex diff --git a/homeassistant/components/homewizard/diagnostics.py b/homeassistant/components/homewizard/diagnostics.py index a97d2507098..a0c852cf4b6 100644 --- a/homeassistant/components/homewizard/diagnostics.py +++ b/homeassistant/components/homewizard/diagnostics.py @@ -27,6 +27,9 @@ async def async_get_config_entry_diagnostics( "state": asdict(coordinator.data["state"]) if coordinator.data["state"] is not None else None, + "system": asdict(coordinator.data["system"]) + if coordinator.data["system"] is not None + else None, } return { diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 97b3c80b50d..baec844cc26 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/homewizard", "codeowners": ["@DCSBL"], "dependencies": [], - "requirements": ["python-homewizard-energy==1.1.0"], + "requirements": ["python-homewizard-energy==1.3.1"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index eca8a7670be..7bf7d9a741e 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -30,6 +30,13 @@ async def async_setup_entry( ] ) + if coordinator.data["system"]: + async_add_entities( + [ + HWEnergyEnableCloudEntity(hass, coordinator, entry), + ] + ) + class HWEnergySwitchEntity( CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SwitchEntity @@ -124,3 +131,47 @@ class HWEnergySwitchLockEntity(HWEnergySwitchEntity): def is_on(self) -> bool: """Return true if switch is on.""" return bool(self.coordinator.data["state"].switch_lock) + + +class HWEnergyEnableCloudEntity(HWEnergySwitchEntity): + """ + Representation of the enable cloud configuration. + + Turning off 'cloud connection' turns off all communication to HomeWizard Cloud. + At this point, the device is fully local. + """ + + _attr_name = "Cloud connection" + _attr_device_class = SwitchDeviceClass.SWITCH + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, + hass: HomeAssistant, + coordinator: HWEnergyDeviceUpdateCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize the switch.""" + super().__init__(coordinator, entry, "cloud_connection") + self.hass = hass + self.entry = entry + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn cloud connection on.""" + await self.coordinator.api.system_set(cloud_enabled=True) + await self.coordinator.async_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn cloud connection off.""" + await self.coordinator.api.system_set(cloud_enabled=False) + await self.coordinator.async_refresh() + + @property + def icon(self) -> str | None: + """Return the icon.""" + return "mdi:cloud" if self.is_on else "mdi:cloud-off-outline" + + @property + def is_on(self) -> bool: + """Return true if cloud connection is active.""" + return bool(self.coordinator.data["system"].cloud_enabled) diff --git a/requirements_all.txt b/requirements_all.txt index ec36b71d2cb..b01377d3f27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2003,7 +2003,7 @@ python-gc100==1.0.3a0 python-gitlab==1.6.0 # homeassistant.components.homewizard -python-homewizard-energy==1.1.0 +python-homewizard-energy==1.3.1 # homeassistant.components.hp_ilo python-hpilo==4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b60432568a1..cf1b928e6a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1399,7 +1399,7 @@ python-forecastio==1.4.0 python-fullykiosk==0.0.11 # homeassistant.components.homewizard -python-homewizard-energy==1.1.0 +python-homewizard-energy==1.3.1 # homeassistant.components.izone python-izone==1.2.9 diff --git a/tests/components/homewizard/conftest.py b/tests/components/homewizard/conftest.py index 1617db35458..c9a04c55dae 100644 --- a/tests/components/homewizard/conftest.py +++ b/tests/components/homewizard/conftest.py @@ -2,7 +2,8 @@ import json from unittest.mock import AsyncMock, patch -from homewizard_energy.models import Data, Device, State +from homewizard_energy.features import Features +from homewizard_energy.models import Data, Device, State, System import pytest from homeassistant.components.homewizard.const import DOMAIN @@ -37,26 +38,32 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture def mock_homewizardenergy(): - """Return a mocked P1 meter.""" + """Return a mocked all-feature device.""" with patch( "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", ) as device: client = device.return_value + client.features = AsyncMock(return_value=Features("HWE-SKT", "3.01")) client.device = AsyncMock( - return_value=Device.from_dict( + side_effect=lambda: Device.from_dict( json.loads(load_fixture("homewizard/device.json")) ) ) client.data = AsyncMock( - return_value=Data.from_dict( + side_effect=lambda: Data.from_dict( json.loads(load_fixture("homewizard/data.json")) ) ) client.state = AsyncMock( - return_value=State.from_dict( + side_effect=lambda: State.from_dict( json.loads(load_fixture("homewizard/state.json")) ) ) + client.system = AsyncMock( + side_effect=lambda: System.from_dict( + json.loads(load_fixture("homewizard/system.json")) + ) + ) yield device diff --git a/tests/components/homewizard/fixtures/system.json b/tests/components/homewizard/fixtures/system.json new file mode 100644 index 00000000000..362491b3519 --- /dev/null +++ b/tests/components/homewizard/fixtures/system.json @@ -0,0 +1,3 @@ +{ + "cloud_enabled": true +} diff --git a/tests/components/homewizard/generator.py b/tests/components/homewizard/generator.py index 0f94580ad84..dff1a4462d3 100644 --- a/tests/components/homewizard/generator.py +++ b/tests/components/homewizard/generator.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock +from homewizard_energy.features import Features from homewizard_energy.models import Device @@ -10,6 +11,7 @@ def get_mock_device( host="1.2.3.4", product_name="P1 meter", product_type="HWE-P1", + firmware_version="1.00", ): """Return a mock bridge.""" mock_device = AsyncMock() @@ -21,11 +23,15 @@ def get_mock_device( product_type=product_type, serial=serial, api_version="V1", - firmware_version="1.00", + firmware_version=firmware_version, ) ) mock_device.data = AsyncMock(return_value=None) mock_device.state = AsyncMock(return_value=None) + mock_device.system = AsyncMock(return_value=None) + mock_device.features = AsyncMock( + return_value=Features(product_type, firmware_version) + ) mock_device.close = AsyncMock() diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py index 899bfb5fb2f..ae703c58cfd 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -45,5 +45,6 @@ async def test_diagnostics( "total_liter_m3": 1234.567, }, "state": {"power_on": True, "switch_lock": False, "brightness": 255}, + "system": {"cloud_enabled": True}, }, } diff --git a/tests/components/homewizard/test_switch.py b/tests/components/homewizard/test_switch.py index 64fb5a56909..a964d548dd3 100644 --- a/tests/components/homewizard/test_switch.py +++ b/tests/components/homewizard/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from homewizard_energy.models import State +from homewizard_energy.models import State, System from homeassistant.components import switch from homeassistant.components.switch import SwitchDeviceClass @@ -286,3 +286,63 @@ async def test_switch_lock_sets_power_on_unavailable( == STATE_OFF ) assert len(api.state_set.mock_calls) == 2 + + +async def test_cloud_connection_on_off(hass, mock_config_entry_data, mock_config_entry): + """Test entity turns switch on and off.""" + + api = get_mock_device(product_type="HWE-SKT", firmware_version="3.02") + api.system = AsyncMock(return_value=System.from_dict({"cloud_enabled": False})) + + def system_set(cloud_enabled): + api.system = AsyncMock( + return_value=System.from_dict({"cloud_enabled": cloud_enabled}) + ) + + api.system_set = AsyncMock(side_effect=system_set) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert ( + hass.states.get("switch.product_name_aabbccddeeff_cloud_connection").state + == STATE_OFF + ) + + # Enable cloud + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) + + await hass.async_block_till_done() + assert len(api.system_set.mock_calls) == 1 + assert ( + hass.states.get("switch.product_name_aabbccddeeff_cloud_connection").state + == STATE_ON + ) + + # Disable cloud + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) + + await hass.async_block_till_done() + assert ( + hass.states.get("switch.product_name_aabbccddeeff_cloud_connection").state + == STATE_OFF + ) + assert len(api.system_set.mock_calls) == 2