From 41097720101669ba06baedf583735628c8232487 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Sun, 24 Oct 2021 16:34:38 +0000 Subject: [PATCH 01/28] add ota update services for block devices --- homeassistant/components/shelly/__init__.py | 68 +++++++++++++++++++ homeassistant/components/shelly/const.py | 2 + homeassistant/components/shelly/service.py | 32 +++++++++ homeassistant/components/shelly/services.yaml | 19 ++++++ 4 files changed, 121 insertions(+) create mode 100644 homeassistant/components/shelly/service.py create mode 100644 homeassistant/components/shelly/services.yaml diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index ca690e33361e38..c7a80163515a46 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -56,6 +56,7 @@ SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, ) +from .service import async_services_setup from .utils import ( get_block_device_name, get_block_device_sleep_period, @@ -182,6 +183,8 @@ def _async_device_online(_: Any) -> None: _LOGGER.debug("Setting up offline block device %s", entry.title) await async_block_device_setup(hass, entry, device) + await async_services_setup(hass) + return True @@ -229,6 +232,8 @@ async def async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool hass.config_entries.async_setup_platforms(entry, RPC_PLATFORMS) + await async_services_setup(hass) + return True @@ -273,6 +278,9 @@ def __init__( self._last_mode: str | None = None self._last_effect: int | None = None + self._ota_update_pending = False + self._ota_update_params = {"beta": False} + entry.async_on_unload( self.async_add_listener(self._async_device_updates_handler) ) @@ -306,6 +314,10 @@ def _async_device_updates_handler(self) -> None: break + # check if OTA update is scheduled + if self._ota_update_pending: + self.hass.async_create_task(self._async_do_ota_update()) + # Check for input events and config change cfg_changed = 0 for block in self.device.blocks: @@ -411,6 +423,62 @@ def async_setup(self) -> None: self.device_id = entry.id self.device.subscribe_updates(self.async_set_updated_data) + async def async_trigger_ota_update(self, beta: bool = False) -> None: + """Trigger or schedule an ota update.""" + update_data = self.device.status["update"] + _LOGGER.debug("OTA update service - update_data: %s", update_data) + + if self._ota_update_pending: + _LOGGER.error( + "OTA update already scheduled for sleeping device %s", self.name + ) + return + + if not update_data["has_update"] and not beta: + _LOGGER.error("No OTA update available for device %s", self.name) + return + + if beta and not update_data.get("beta_version") and not beta: + _LOGGER.error( + "No OTA update on beta channel available for device %s", self.name + ) + return + + if update_data["status"] == "updating": + _LOGGER.error("OTA update already in progress for %s", self.name) + return + + if self.entry.data.get(CONF_SLEEP_PERIOD): + self._ota_update_pending = True + self._ota_update_params = {"beta": beta} + _LOGGER.info("OTA update scheduled for sleeping device %s", self.name) + else: + self._ota_update_params = {"beta": beta} + await self._async_do_ota_update() + + async def _async_do_ota_update(self) -> None: + """Perform an ota update.""" + beta_channel = self._ota_update_params["beta"] + update_data = self.device.status["update"] + new_version = update_data["new_version"] + if beta_channel: + new_version = update_data["beta_version"] + + _LOGGER.info( + "Start OTA update of device %s from '%s' to '%s'", + self.name, + update_data["old_version"], + new_version, + ) + result = None + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + result = await self.device.trigger_ota_update(beta=beta_channel) + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error while perform ota update: %s", err) + + _LOGGER.debug("Result of OTA update call: %s", result) + def shutdown(self) -> None: """Shutdown the wrapper.""" self.device.shutdown() diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index d241998a138946..33b5987c065147 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -10,6 +10,8 @@ DOMAIN: Final = "shelly" REST: Final = "rest" RPC: Final = "rpc" +SERVICE_OTA_UPDATE: Final = "ota_update" +SERVICES: Final = [SERVICE_OTA_UPDATE] CONF_COAP_PORT: Final = "coap_port" DEFAULT_COAP_PORT: Final = 5683 diff --git a/homeassistant/components/shelly/service.py b/homeassistant/components/shelly/service.py new file mode 100644 index 00000000000000..ef8c615d030630 --- /dev/null +++ b/homeassistant/components/shelly/service.py @@ -0,0 +1,32 @@ +"""Services for the Shelly integration.""" +from __future__ import annotations + +from homeassistant.const import ATTR_AREA_ID, ATTR_DEVICE_ID +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.service import async_extract_config_entry_ids + +from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, SERVICE_OTA_UPDATE + + +async def async_services_setup(hass: HomeAssistant) -> None: + """Set up services.""" + + async def async_service_ota_update(call: ServiceCall) -> None: + """Trigger OTA update.""" + if not (call.data.get(ATTR_DEVICE_ID) or call.data.get(ATTR_AREA_ID)): + raise HomeAssistantError("No target selected for OTA update") + + beta_channel = bool(call.data.get("beta")) + + entry_ids = await async_extract_config_entry_ids(hass, call) + for entry_id in entry_ids: + entry = hass.config_entries.async_get_entry(entry_id) + if entry and entry.domain == DOMAIN: + if active_entry := hass.data[DOMAIN][DATA_CONFIG_ENTRY].get( + entry.entry_id + ): + if block_wrapper := active_entry.get(BLOCK): + await block_wrapper.async_trigger_ota_update(beta=beta_channel) + + hass.services.async_register(DOMAIN, SERVICE_OTA_UPDATE, async_service_ota_update) diff --git a/homeassistant/components/shelly/services.yaml b/homeassistant/components/shelly/services.yaml new file mode 100644 index 00000000000000..5b640c295c316e --- /dev/null +++ b/homeassistant/components/shelly/services.yaml @@ -0,0 +1,19 @@ +# shelly service descriptions. + +ota_update: + name: OTA Update + description: Trigger an over-the-air (OTA) update. + target: + device: + integration: shelly + entity: + integration: none + fields: + beta: + name: Beta + description: Run firmware update to beta version (if available) + required: false + default: false + example: true + selector: + boolean: From a0770e98d22ddbe90a8e8a0836144a3a543a3793 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Sun, 24 Oct 2021 22:20:01 +0000 Subject: [PATCH 02/28] add ota update services for rpc devices --- homeassistant/components/shelly/__init__.py | 44 +++++++++++++++++++++ homeassistant/components/shelly/service.py | 5 ++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index c7a80163515a46..94871d49eb1f76 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -617,6 +617,8 @@ def __init__( self.entry = entry self.device = device + self._ota_update_params = {"beta": False} + self._debounced_reload = Debouncer( hass, _LOGGER, @@ -716,6 +718,48 @@ def async_setup(self) -> None: self.device_id = entry.id self.device.subscribe_updates(self.async_set_updated_data) + async def async_trigger_ota_update(self, beta: bool = False) -> None: + """Trigger or schedule an ota update.""" + update_data = self.device.status["sys"]["available_updates"] + _LOGGER.debug("OTA update service - update_data: %s", update_data) + + if not bool(update_data) or (not update_data.get("stable") and not beta): + _LOGGER.error("No OTA update available for device %s", self.name) + return + + if beta and not update_data.get("beta"): + _LOGGER.error( + "No OTA update on beta channel available for device %s", self.name + ) + return + + self._ota_update_params = {"beta": beta} + await self._async_do_ota_update() + + async def _async_do_ota_update(self) -> None: + """Perform an ota update.""" + beta_channel = self._ota_update_params["beta"] + update_data = self.device.status["sys"]["available_updates"] + new_version = update_data.get("stable", {"version": ""})["version"] + if beta_channel: + new_version = update_data.get("beta", {"version": ""})["version"] + + assert self.device.shelly + _LOGGER.info( + "Start OTA update of device %s from '%s' to '%s'", + self.name, + self.device.shelly["fw"], + new_version, + ) + result = None + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + result = await self.device.trigger_ota_update(beta=beta_channel) + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error while perform ota update: %s", err) + + _LOGGER.debug("Result of OTA update call: %s", result) + async def shutdown(self) -> None: """Shutdown the wrapper.""" await self.device.shutdown() diff --git a/homeassistant/components/shelly/service.py b/homeassistant/components/shelly/service.py index ef8c615d030630..c5b9f705c70bdd 100644 --- a/homeassistant/components/shelly/service.py +++ b/homeassistant/components/shelly/service.py @@ -6,7 +6,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.service import async_extract_config_entry_ids -from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, SERVICE_OTA_UPDATE +from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC, SERVICE_OTA_UPDATE async def async_services_setup(hass: HomeAssistant) -> None: @@ -29,4 +29,7 @@ async def async_service_ota_update(call: ServiceCall) -> None: if block_wrapper := active_entry.get(BLOCK): await block_wrapper.async_trigger_ota_update(beta=beta_channel) + if rpc_wrapper := active_entry.get(RPC): + await rpc_wrapper.async_trigger_ota_update(beta=beta_channel) + hass.services.async_register(DOMAIN, SERVICE_OTA_UPDATE, async_service_ota_update) From 80d6a433bfa0938fa1b23502d1069a9d1bc0c4a0 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Sun, 24 Oct 2021 22:20:17 +0000 Subject: [PATCH 03/28] add tests --- tests/components/shelly/conftest.py | 24 ++++++- tests/components/shelly/test_service.py | 84 +++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 tests/components/shelly/test_service.py diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index a0d4a27bbc40bd..16dc8fdae9ef23 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -71,8 +71,25 @@ "num_outputs": 2, } -MOCK_STATUS = { +MOCK_STATUS_COAP = { + "update": { + "status": "pending", + "has_update": True, + "beta_version": "some_beta_version", + "new_version": "some_new_version", + "old_version": "some_old_version", + }, +} + + +MOCK_STATUS_RPC = { "switch:0": {"output": True}, + "sys": { + "available_updates": { + "beta": {"version": "some_beta_version"}, + "stable": {"version": "some_beta_version"}, + } + }, } @@ -117,8 +134,10 @@ async def coap_wrapper(hass): blocks=MOCK_BLOCKS, settings=MOCK_SETTINGS, shelly=MOCK_SHELLY, + status=MOCK_STATUS_COAP, firmware_version="some fw string", update=AsyncMock(), + trigger_ota_update=AsyncMock(), initialized=True, ) @@ -150,9 +169,10 @@ async def rpc_wrapper(hass): config=MOCK_CONFIG, event={}, shelly=MOCK_SHELLY, - status=MOCK_STATUS, + status=MOCK_STATUS_RPC, firmware_version="some fw string", update=AsyncMock(), + trigger_ota_update=AsyncMock(), initialized=True, shutdown=AsyncMock(), ) diff --git a/tests/components/shelly/test_service.py b/tests/components/shelly/test_service.py new file mode 100644 index 00000000000000..83f65f0369814b --- /dev/null +++ b/tests/components/shelly/test_service.py @@ -0,0 +1,84 @@ +"""Tests for the Shelly integration init.""" +import pytest + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.shelly.const import DOMAIN, SERVICE_OTA_UPDATE, SERVICES +from homeassistant.components.shelly.service import async_services_setup +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry, + async_get, +) + + +async def test_services_registered(hass): + """Test if all services are registered.""" + await async_services_setup(hass) + for service in SERVICES: + assert hass.services.has_service(DOMAIN, service) + + +async def test_service_error(hass): + """Test for errors in service call.""" + await async_services_setup(hass) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_OTA_UPDATE, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + +async def test_service_ota_coap_device(hass, coap_wrapper): + """Test OTA update service with block device.""" + assert coap_wrapper + hass.async_create_task( + hass.config_entries.async_forward_entry_setup( + coap_wrapper.entry, BINARY_SENSOR_DOMAIN + ) + ) + await hass.async_block_till_done() + + dev_reg = async_get(hass) + devices = async_entries_for_config_entry(dev_reg, coap_wrapper.entry.entry_id) + assert devices + assert devices[0] + + await async_services_setup(hass) + await hass.services.async_call( + DOMAIN, + SERVICE_OTA_UPDATE, + {ATTR_DEVICE_ID: devices[0].id}, + blocking=True, + ) + await hass.async_block_till_done() + coap_wrapper.device.trigger_ota_update.assert_called_once() + + +async def test_service_ota_rpc_device(hass, rpc_wrapper): + """Test OTA update service with rpc device.""" + assert rpc_wrapper + hass.async_create_task( + hass.config_entries.async_forward_entry_setup( + rpc_wrapper.entry, BINARY_SENSOR_DOMAIN + ) + ) + await hass.async_block_till_done() + + dev_reg = async_get(hass) + devices = async_entries_for_config_entry(dev_reg, rpc_wrapper.entry.entry_id) + assert devices + assert devices[0] + + await async_services_setup(hass) + await hass.services.async_call( + DOMAIN, + SERVICE_OTA_UPDATE, + {ATTR_DEVICE_ID: devices[0].id}, + blocking=True, + ) + await hass.async_block_till_done() + rpc_wrapper.device.trigger_ota_update.assert_called_once() From fd83c8fc7a7f934e701ac0b2b84490232460ce42 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Mon, 25 Oct 2021 16:37:09 +0000 Subject: [PATCH 04/28] use get_device_entry_gen() --- homeassistant/components/shelly/service.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/shelly/service.py b/homeassistant/components/shelly/service.py index c5b9f705c70bdd..4b2cfa5fe623bb 100644 --- a/homeassistant/components/shelly/service.py +++ b/homeassistant/components/shelly/service.py @@ -7,6 +7,7 @@ from homeassistant.helpers.service import async_extract_config_entry_ids from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC, SERVICE_OTA_UPDATE +from .utils import get_device_entry_gen async def async_services_setup(hass: HomeAssistant) -> None: @@ -22,14 +23,14 @@ async def async_service_ota_update(call: ServiceCall) -> None: entry_ids = await async_extract_config_entry_ids(hass, call) for entry_id in entry_ids: entry = hass.config_entries.async_get_entry(entry_id) - if entry and entry.domain == DOMAIN: - if active_entry := hass.data[DOMAIN][DATA_CONFIG_ENTRY].get( - entry.entry_id - ): - if block_wrapper := active_entry.get(BLOCK): - await block_wrapper.async_trigger_ota_update(beta=beta_channel) - - if rpc_wrapper := active_entry.get(RPC): - await rpc_wrapper.async_trigger_ota_update(beta=beta_channel) + if not (entry and entry.domain == DOMAIN): + continue + + if active_entry := hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(entry.entry_id): + if get_device_entry_gen(entry) == 2: + wrapper = active_entry.get(RPC) + else: + wrapper = active_entry.get(BLOCK) + await wrapper.async_trigger_ota_update(beta=beta_channel) hass.services.async_register(DOMAIN, SERVICE_OTA_UPDATE, async_service_ota_update) From fed8f32e8f740932cd6f935405db416ac2026bd7 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Thu, 4 Nov 2021 21:04:09 +0000 Subject: [PATCH 05/28] add button entity --- homeassistant/components/shelly/__init__.py | 13 +++- homeassistant/components/shelly/button.py | 70 +++++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/shelly/button.py diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 94871d49eb1f76..17930e0163bf18 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -65,9 +65,16 @@ get_rpc_device_name, ) -BLOCK_PLATFORMS: Final = ["binary_sensor", "cover", "light", "sensor", "switch"] -BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "sensor"] -RPC_PLATFORMS: Final = ["binary_sensor", "light", "sensor", "switch"] +BLOCK_PLATFORMS: Final = [ + "binary_sensor", + "button", + "cover", + "light", + "sensor", + "switch", +] +BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "button", "sensor"] +RPC_PLATFORMS: Final = ["binary_sensor", "button", "light", "sensor", "switch"] _LOGGER: Final = logging.getLogger(__name__) COAP_SCHEMA: Final = vol.Schema( diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py new file mode 100644 index 00000000000000..cd311df9d227cd --- /dev/null +++ b/homeassistant/components/shelly/button.py @@ -0,0 +1,70 @@ +"""Demo platform that offers a fake button entity.""" +from __future__ import annotations + +from typing import cast + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_DEVICE_ID, ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from . import BlockDeviceWrapper, RpcDeviceWrapper +from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC, SERVICE_OTA_UPDATE +from .utils import get_block_device_name, get_device_entry_gen, get_rpc_device_name + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Demo config entry.""" + wrapper: RpcDeviceWrapper | BlockDeviceWrapper | None = None + if get_device_entry_gen(config_entry) == 2: + if rpc_wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + config_entry.entry_id + ].get(RPC): + wrapper = cast(RpcDeviceWrapper, rpc_wrapper) + else: + if block_wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + config_entry.entry_id + ].get(BLOCK): + wrapper = cast(BlockDeviceWrapper, block_wrapper) + + if wrapper is not None: + async_add_entities([ShellyOtaUpdateButton(wrapper, config_entry)]) + + +class ShellyOtaUpdateButton(ButtonEntity): + """Defines a Shelly OTA update button.""" + + _attr_icon = "mdi:sync" + _attr_entity_category = ENTITY_CATEGORY_CONFIG + + def __init__( + self, wrapper: RpcDeviceWrapper | BlockDeviceWrapper, entry: ConfigEntry + ) -> None: + """Initialize Shelly OTA update button.""" + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, wrapper.mac)} + ) + + if isinstance(wrapper, RpcDeviceWrapper): + device_name = get_rpc_device_name(wrapper.device) + else: + device_name = get_block_device_name(wrapper.device) + + self._attr_name = f"{device_name} OTA Update" + self._attr_unique_id = slugify(self._attr_name) + + self.wrapper = wrapper + + async def async_press(self) -> None: + """Send out a persistent notification.""" + await self.hass.services.async_call( + DOMAIN, SERVICE_OTA_UPDATE, {ATTR_DEVICE_ID: self.wrapper.device_id} + ) From 95be88dea9bfe09d132f0dd9e629304bddef5a23 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Thu, 4 Nov 2021 22:22:01 +0000 Subject: [PATCH 06/28] add ota beta channel switch --- homeassistant/components/shelly/button.py | 17 +++++++- homeassistant/components/shelly/const.py | 1 + homeassistant/components/shelly/switch.py | 53 ++++++++++++++++++++++- 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index cd311df9d227cd..40a1e1818d0fc9 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -13,7 +13,14 @@ from homeassistant.util import slugify from . import BlockDeviceWrapper, RpcDeviceWrapper -from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC, SERVICE_OTA_UPDATE +from .const import ( + BLOCK, + CONF_OTA_BETA_CHANNEL, + DATA_CONFIG_ENTRY, + DOMAIN, + RPC, + SERVICE_OTA_UPDATE, +) from .utils import get_block_device_name, get_device_entry_gen, get_rpc_device_name @@ -61,10 +68,16 @@ def __init__( self._attr_name = f"{device_name} OTA Update" self._attr_unique_id = slugify(self._attr_name) + self.entry = entry self.wrapper = wrapper async def async_press(self) -> None: """Send out a persistent notification.""" await self.hass.services.async_call( - DOMAIN, SERVICE_OTA_UPDATE, {ATTR_DEVICE_ID: self.wrapper.device_id} + DOMAIN, + SERVICE_OTA_UPDATE, + { + ATTR_DEVICE_ID: self.wrapper.device_id, + "beta": self.entry.options.get(CONF_OTA_BETA_CHANNEL), + }, ) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 33b5987c065147..87e6e8b9a87b27 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -92,6 +92,7 @@ ATTR_DEVICE: Final = "device" ATTR_GENERATION: Final = "generation" CONF_SUBTYPE: Final = "subtype" +CONF_OTA_BETA_CHANNEL: Final = "ota_beta_channel" BASIC_INPUTS_EVENTS_TYPES: Final = {"single", "long"} diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 0291258b511db3..6bd48ec746fd05 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -7,15 +7,21 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify from . import BlockDeviceWrapper, RpcDeviceWrapper -from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC +from .const import BLOCK, CONF_OTA_BETA_CHANNEL, DATA_CONFIG_ENTRY, DOMAIN, RPC from .entity import ShellyBlockEntity, ShellyRpcEntity from .utils import ( async_remove_shelly_entity, + get_block_device_name, get_device_entry_gen, + get_rpc_device_name, get_rpc_key_ids, is_block_channel_type_light, is_rpc_channel_type_light, @@ -65,6 +71,7 @@ async def async_setup_block_entry( return async_add_entities(BlockRelaySwitch(wrapper, block) for block in relay_blocks) + async_add_entities([ShellyOtaUpdateBetaChannelSwitch(wrapper, config_entry)]) async def async_setup_rpc_entry( @@ -89,6 +96,7 @@ async def async_setup_rpc_entry( return async_add_entities(RpcRelaySwitch(wrapper, id_) for id_ in switch_ids) + async_add_entities([ShellyOtaUpdateBetaChannelSwitch(wrapper, config_entry)]) class BlockRelaySwitch(ShellyBlockEntity, SwitchEntity): @@ -144,3 +152,46 @@ async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None: """Turn off relay.""" await self.call_rpc("Switch.Set", {"id": self._id, "on": False}) + + +class ShellyOtaUpdateBetaChannelSwitch(SwitchEntity): + """Defines a Shelly OTA update beta channel switch.""" + + _attr_icon = "mdi:flask-outline" + _attr_entity_category = ENTITY_CATEGORY_CONFIG + + def __init__( + self, wrapper: RpcDeviceWrapper | BlockDeviceWrapper, entry: ConfigEntry + ) -> None: + """Initialize Shelly OTA update beta channel switch.""" + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, wrapper.mac)} + ) + + if isinstance(wrapper, RpcDeviceWrapper): + device_name = get_rpc_device_name(wrapper.device) + else: + device_name = get_block_device_name(wrapper.device) + + self._attr_name = f"{device_name} OTA Update Beta channel" + self._attr_unique_id = slugify(self._attr_name) + + self.entry = entry + self.wrapper = wrapper + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return bool(self.entry.options.get(CONF_OTA_BETA_CHANNEL)) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + self.hass.config_entries.async_update_entry( + self.entry, options={**self.entry.options, CONF_OTA_BETA_CHANNEL: True} + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + self.hass.config_entries.async_update_entry( + self.entry, options={**self.entry.options, CONF_OTA_BETA_CHANNEL: False} + ) From 4ae51b7e33a9a512033e8fdd58ff3c0098a27a2a Mon Sep 17 00:00:00 2001 From: mib1185 Date: Thu, 4 Nov 2021 22:23:11 +0000 Subject: [PATCH 07/28] fix check beta channel for block device --- homeassistant/components/shelly/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 17930e0163bf18..37bc81ddb00cca 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -445,7 +445,7 @@ async def async_trigger_ota_update(self, beta: bool = False) -> None: _LOGGER.error("No OTA update available for device %s", self.name) return - if beta and not update_data.get("beta_version") and not beta: + if beta and not update_data.get("beta_version"): _LOGGER.error( "No OTA update on beta channel available for device %s", self.name ) From fd21def87221008749be14463cd5fa43b12d30b1 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Thu, 4 Nov 2021 23:03:17 +0000 Subject: [PATCH 08/28] use constant for beta attribute --- homeassistant/components/shelly/__init__.py | 19 ++++++++++--------- .../components/shelly/binary_sensor.py | 4 ++-- homeassistant/components/shelly/button.py | 3 ++- homeassistant/components/shelly/const.py | 1 + homeassistant/components/shelly/service.py | 4 ++-- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 37bc81ddb00cca..a7c87566490505 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -29,6 +29,7 @@ from .const import ( AIOSHELLY_DEVICE_TIMEOUT_SEC, + ATTR_BETA, ATTR_CHANNEL, ATTR_CLICK_TYPE, ATTR_DEVICE, @@ -286,7 +287,7 @@ def __init__( self._last_effect: int | None = None self._ota_update_pending = False - self._ota_update_params = {"beta": False} + self._ota_update_params = {ATTR_BETA: False} entry.async_on_unload( self.async_add_listener(self._async_device_updates_handler) @@ -457,15 +458,15 @@ async def async_trigger_ota_update(self, beta: bool = False) -> None: if self.entry.data.get(CONF_SLEEP_PERIOD): self._ota_update_pending = True - self._ota_update_params = {"beta": beta} + self._ota_update_params = {ATTR_BETA: beta} _LOGGER.info("OTA update scheduled for sleeping device %s", self.name) else: - self._ota_update_params = {"beta": beta} + self._ota_update_params = {ATTR_BETA: beta} await self._async_do_ota_update() async def _async_do_ota_update(self) -> None: """Perform an ota update.""" - beta_channel = self._ota_update_params["beta"] + beta_channel = self._ota_update_params[ATTR_BETA] update_data = self.device.status["update"] new_version = update_data["new_version"] if beta_channel: @@ -624,7 +625,7 @@ def __init__( self.entry = entry self.device = device - self._ota_update_params = {"beta": False} + self._ota_update_params = {ATTR_BETA: False} self._debounced_reload = Debouncer( hass, @@ -734,22 +735,22 @@ async def async_trigger_ota_update(self, beta: bool = False) -> None: _LOGGER.error("No OTA update available for device %s", self.name) return - if beta and not update_data.get("beta"): + if beta and not update_data.get(ATTR_BETA): _LOGGER.error( "No OTA update on beta channel available for device %s", self.name ) return - self._ota_update_params = {"beta": beta} + self._ota_update_params = {ATTR_BETA: beta} await self._async_do_ota_update() async def _async_do_ota_update(self) -> None: """Perform an ota update.""" - beta_channel = self._ota_update_params["beta"] + beta_channel = self._ota_update_params[ATTR_BETA] update_data = self.device.status["sys"]["available_updates"] new_version = update_data.get("stable", {"version": ""})["version"] if beta_channel: - new_version = update_data.get("beta", {"version": ""})["version"] + new_version = update_data.get(ATTR_BETA, {"version": ""})["version"] assert self.device.shelly _LOGGER.info( diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 4a918506b00811..276cf7b87f6c4e 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -17,7 +17,7 @@ STATE_ON, BinarySensorEntity, ) -from homeassistant.components.shelly.const import CONF_SLEEP_PERIOD +from homeassistant.components.shelly.const import ATTR_BETA, CONF_SLEEP_PERIOD from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant @@ -158,7 +158,7 @@ extra_state_attributes=lambda status, shelly: { "latest_stable_version": status.get("stable", {"version": ""})["version"], "installed_version": shelly["ver"], - "beta_version": status.get("beta", {"version": ""})["version"], + "beta_version": status.get(ATTR_BETA, {"version": ""})["version"], }, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 40a1e1818d0fc9..a064838e18f135 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -14,6 +14,7 @@ from . import BlockDeviceWrapper, RpcDeviceWrapper from .const import ( + ATTR_BETA, BLOCK, CONF_OTA_BETA_CHANNEL, DATA_CONFIG_ENTRY, @@ -78,6 +79,6 @@ async def async_press(self) -> None: SERVICE_OTA_UPDATE, { ATTR_DEVICE_ID: self.wrapper.device_id, - "beta": self.entry.options.get(CONF_OTA_BETA_CHANNEL), + ATTR_BETA: self.entry.options.get(CONF_OTA_BETA_CHANNEL), }, ) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 87e6e8b9a87b27..ecd36ca62a63ba 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -92,6 +92,7 @@ ATTR_DEVICE: Final = "device" ATTR_GENERATION: Final = "generation" CONF_SUBTYPE: Final = "subtype" +ATTR_BETA: Final = "beta" CONF_OTA_BETA_CHANNEL: Final = "ota_beta_channel" BASIC_INPUTS_EVENTS_TYPES: Final = {"single", "long"} diff --git a/homeassistant/components/shelly/service.py b/homeassistant/components/shelly/service.py index 4b2cfa5fe623bb..15aa127630647f 100644 --- a/homeassistant/components/shelly/service.py +++ b/homeassistant/components/shelly/service.py @@ -6,7 +6,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.service import async_extract_config_entry_ids -from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC, SERVICE_OTA_UPDATE +from .const import ATTR_BETA, BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC, SERVICE_OTA_UPDATE from .utils import get_device_entry_gen @@ -18,7 +18,7 @@ async def async_service_ota_update(call: ServiceCall) -> None: if not (call.data.get(ATTR_DEVICE_ID) or call.data.get(ATTR_AREA_ID)): raise HomeAssistantError("No target selected for OTA update") - beta_channel = bool(call.data.get("beta")) + beta_channel = bool(call.data.get(ATTR_BETA)) entry_ids = await async_extract_config_entry_ids(hass, call) for entry_id in entry_ids: From 46089ef2acb0d15299e82430f9aefc028d33f28c Mon Sep 17 00:00:00 2001 From: mib1185 Date: Thu, 4 Nov 2021 23:47:12 +0000 Subject: [PATCH 09/28] add tests for button and switch --- tests/components/shelly/test_button.py | 54 ++++++++++++++++++++++ tests/components/shelly/test_switch.py | 64 ++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 tests/components/shelly/test_button.py diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py new file mode 100644 index 00000000000000..1a1c679f304bda --- /dev/null +++ b/tests/components/shelly/test_button.py @@ -0,0 +1,54 @@ +"""The scene tests for the myq platform.""" +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.components.shelly.service import async_services_setup +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + + +async def test_block_button(hass: HomeAssistant, coap_wrapper): + """Test block device OTA button.""" + assert coap_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, BUTTON_DOMAIN) + ) + await hass.async_block_till_done() + + state = hass.states.get("button.test_name_ota_update") + assert state + assert state.state == STATE_UNKNOWN + + await async_services_setup(hass) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.test_name_ota_update"}, + blocking=True, + ) + await hass.async_block_till_done() + coap_wrapper.device.trigger_ota_update.assert_called_once() + + +async def test_rpc_button(hass: HomeAssistant, rpc_wrapper): + """Test rpc device OTA button.""" + assert rpc_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, BUTTON_DOMAIN) + ) + await hass.async_block_till_done() + + state = hass.states.get("button.test_name_ota_update") + assert state + assert state.state == STATE_UNKNOWN + + await async_services_setup(hass) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.test_name_ota_update"}, + blocking=True, + ) + await hass.async_block_till_done() + rpc_wrapper.device.trigger_ota_update.assert_called_once() diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index fc61102507b1c1..ce7f1dbbd339f8 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -85,6 +85,38 @@ async def test_block_device_mode_roller(hass, coap_wrapper, monkeypatch): assert hass.states.get("switch.test_name_channel_1") is None +async def test_block_ota_beta_switch(hass, coap_wrapper): + """Test block device OTA update beta channel switch.""" + assert coap_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.test_name_ota_update_beta_channel") + assert state + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.test_name_ota_update_beta_channel"}, + blocking=True, + ) + assert hass.states.get("switch.test_name_ota_update_beta_channel").state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.test_name_ota_update_beta_channel"}, + blocking=True, + ) + assert ( + hass.states.get("switch.test_name_ota_update_beta_channel").state == STATE_OFF + ) + + async def test_block_device_app_type_light(hass, coap_wrapper, monkeypatch): """Test block device in app type set to light mode.""" assert coap_wrapper @@ -141,3 +173,35 @@ async def test_rpc_device_switch_type_lights_mode(hass, rpc_wrapper, monkeypatch ) await hass.async_block_till_done() assert hass.states.get("switch.test_switch_0") is None + + +async def test_rpc_ota_beta_switch(hass, rpc_wrapper): + """Test rpc device OTA update beta channel switch.""" + assert rpc_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, SWITCH_DOMAIN) + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.test_name_ota_update_beta_channel") + assert state + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.test_name_ota_update_beta_channel"}, + blocking=True, + ) + assert hass.states.get("switch.test_name_ota_update_beta_channel").state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.test_name_ota_update_beta_channel"}, + blocking=True, + ) + assert ( + hass.states.get("switch.test_name_ota_update_beta_channel").state == STATE_OFF + ) From f65acd1be67ff6bf225d2dec52b97ef273286885 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Fri, 5 Nov 2021 00:06:47 +0000 Subject: [PATCH 10/28] improve button tests --- tests/components/shelly/test_button.py | 65 +++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index 1a1c679f304bda..baa8bbda31199f 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -2,6 +2,7 @@ from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.button.const import SERVICE_PRESS from homeassistant.components.shelly.service import async_services_setup +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_ON from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -27,7 +28,37 @@ async def test_block_button(hass: HomeAssistant, coap_wrapper): blocking=True, ) await hass.async_block_till_done() - coap_wrapper.device.trigger_ota_update.assert_called_once() + coap_wrapper.device.trigger_ota_update.assert_called_once_with(beta=False) + + +async def test_block_button_beta(hass: HomeAssistant, coap_wrapper): + """Test block device OTA button with beta channel enabled.""" + assert coap_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, BUTTON_DOMAIN) + ) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) + ) + await hass.async_block_till_done() + + await async_services_setup(hass) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.test_name_ota_update_beta_channel"}, + blocking=True, + ) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.test_name_ota_update"}, + blocking=True, + ) + await hass.async_block_till_done() + coap_wrapper.device.trigger_ota_update.assert_called_once_with(beta=True) async def test_rpc_button(hass: HomeAssistant, rpc_wrapper): @@ -51,4 +82,34 @@ async def test_rpc_button(hass: HomeAssistant, rpc_wrapper): blocking=True, ) await hass.async_block_till_done() - rpc_wrapper.device.trigger_ota_update.assert_called_once() + rpc_wrapper.device.trigger_ota_update.assert_called_once_with(beta=False) + + +async def test_rpc_button_beta(hass: HomeAssistant, rpc_wrapper): + """Test rpc device OTA button with beta channel enabled.""" + assert rpc_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, BUTTON_DOMAIN) + ) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, SWITCH_DOMAIN) + ) + await hass.async_block_till_done() + + await async_services_setup(hass) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.test_name_ota_update_beta_channel"}, + blocking=True, + ) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.test_name_ota_update"}, + blocking=True, + ) + await hass.async_block_till_done() + rpc_wrapper.device.trigger_ota_update.assert_called_once_with(beta=True) From 1fbc64b0ec2bc06beaeb59b27c5926e72f61e177 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Sat, 6 Nov 2021 17:32:25 +0000 Subject: [PATCH 11/28] remove constant from device specific dict --- homeassistant/components/shelly/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 276cf7b87f6c4e..4a918506b00811 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -17,7 +17,7 @@ STATE_ON, BinarySensorEntity, ) -from homeassistant.components.shelly.const import ATTR_BETA, CONF_SLEEP_PERIOD +from homeassistant.components.shelly.const import CONF_SLEEP_PERIOD from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant @@ -158,7 +158,7 @@ extra_state_attributes=lambda status, shelly: { "latest_stable_version": status.get("stable", {"version": ""})["version"], "installed_version": shelly["ver"], - "beta_version": status.get(ATTR_BETA, {"version": ""})["version"], + "beta_version": status.get("beta", {"version": ""})["version"], }, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), From 5965ae5787bd7e910dae2fca8dcf823fdbe92294 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 6 Nov 2021 18:54:07 +0100 Subject: [PATCH 12/28] Apply suggestions from code review Co-authored-by: Shay Levy --- homeassistant/components/shelly/button.py | 2 +- tests/components/shelly/test_button.py | 2 +- tests/components/shelly/test_service.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index a064838e18f135..6ec1702592a753 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -1,4 +1,4 @@ -"""Demo platform that offers a fake button entity.""" +"""Button for Shelly.""" from __future__ import annotations from typing import cast diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index baa8bbda31199f..83c68c3df8dd35 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -1,4 +1,4 @@ -"""The scene tests for the myq platform.""" +"""Tests for Shelly button platform.""" from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.button.const import SERVICE_PRESS from homeassistant.components.shelly.service import async_services_setup diff --git a/tests/components/shelly/test_service.py b/tests/components/shelly/test_service.py index 83f65f0369814b..0caf9793139b5b 100644 --- a/tests/components/shelly/test_service.py +++ b/tests/components/shelly/test_service.py @@ -1,4 +1,4 @@ -"""Tests for the Shelly integration init.""" +"""Tests for the Shelly services.""" import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN From 6dea2c778d16cb98a73780544ded5405e246697f Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 6 Nov 2021 19:57:32 +0100 Subject: [PATCH 13/28] correct descriptions --- homeassistant/components/shelly/button.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 6ec1702592a753..a1a746c1fae8c9 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -30,7 +30,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the Demo config entry.""" + """Set buttons for device.""" wrapper: RpcDeviceWrapper | BlockDeviceWrapper | None = None if get_device_entry_gen(config_entry) == 2: if rpc_wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][ @@ -73,7 +73,7 @@ def __init__( self.wrapper = wrapper async def async_press(self) -> None: - """Send out a persistent notification.""" + """Triggers the OTA update service.""" await self.hass.services.async_call( DOMAIN, SERVICE_OTA_UPDATE, From 1a90bdbae6524ea2dca1d10dfef00d69d4507ccc Mon Sep 17 00:00:00 2001 From: mib1185 Date: Sun, 7 Nov 2021 14:25:14 +0000 Subject: [PATCH 14/28] move do_ota_update into trigger_ota_update funct --- homeassistant/components/shelly/__init__.py | 106 ++++++++++---------- 1 file changed, 55 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index a7c87566490505..a378c4f218afce 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -324,7 +324,9 @@ def _async_device_updates_handler(self) -> None: # check if OTA update is scheduled if self._ota_update_pending: - self.hass.async_create_task(self._async_do_ota_update()) + self.hass.async_create_task( + self.async_trigger_ota_update(**self._ota_update_params) + ) # Check for input events and config change cfg_changed = 0 @@ -433,6 +435,30 @@ def async_setup(self) -> None: async def async_trigger_ota_update(self, beta: bool = False) -> None: """Trigger or schedule an ota update.""" + + async def _async_do_ota_update() -> None: + """Perform an ota update.""" + beta_channel = self._ota_update_params[ATTR_BETA] + update_data = self.device.status["update"] + new_version = update_data["new_version"] + if beta_channel: + new_version = update_data["beta_version"] + + _LOGGER.info( + "Start OTA update of device %s from '%s' to '%s'", + self.name, + update_data["old_version"], + new_version, + ) + result = None + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + result = await self.device.trigger_ota_update(beta=beta_channel) + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error while perform ota update: %s", err) + + _LOGGER.debug("Result of OTA update call: %s", result) + update_data = self.device.status["update"] _LOGGER.debug("OTA update service - update_data: %s", update_data) @@ -462,30 +488,7 @@ async def async_trigger_ota_update(self, beta: bool = False) -> None: _LOGGER.info("OTA update scheduled for sleeping device %s", self.name) else: self._ota_update_params = {ATTR_BETA: beta} - await self._async_do_ota_update() - - async def _async_do_ota_update(self) -> None: - """Perform an ota update.""" - beta_channel = self._ota_update_params[ATTR_BETA] - update_data = self.device.status["update"] - new_version = update_data["new_version"] - if beta_channel: - new_version = update_data["beta_version"] - - _LOGGER.info( - "Start OTA update of device %s from '%s' to '%s'", - self.name, - update_data["old_version"], - new_version, - ) - result = None - try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - result = await self.device.trigger_ota_update(beta=beta_channel) - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error while perform ota update: %s", err) - - _LOGGER.debug("Result of OTA update call: %s", result) + await _async_do_ota_update() def shutdown(self) -> None: """Shutdown the wrapper.""" @@ -727,7 +730,32 @@ def async_setup(self) -> None: self.device.subscribe_updates(self.async_set_updated_data) async def async_trigger_ota_update(self, beta: bool = False) -> None: - """Trigger or schedule an ota update.""" + """Trigger an ota update.""" + + async def _async_do_ota_update() -> None: + """Perform an ota update.""" + beta_channel = self._ota_update_params[ATTR_BETA] + update_data = self.device.status["sys"]["available_updates"] + new_version = update_data.get("stable", {"version": ""})["version"] + if beta_channel: + new_version = update_data.get(ATTR_BETA, {"version": ""})["version"] + + assert self.device.shelly + _LOGGER.info( + "Start OTA update of device %s from '%s' to '%s'", + self.name, + self.device.shelly["fw"], + new_version, + ) + result = None + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + result = await self.device.trigger_ota_update(beta=beta_channel) + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error while perform ota update: %s", err) + + _LOGGER.debug("Result of OTA update call: %s", result) + update_data = self.device.status["sys"]["available_updates"] _LOGGER.debug("OTA update service - update_data: %s", update_data) @@ -742,31 +770,7 @@ async def async_trigger_ota_update(self, beta: bool = False) -> None: return self._ota_update_params = {ATTR_BETA: beta} - await self._async_do_ota_update() - - async def _async_do_ota_update(self) -> None: - """Perform an ota update.""" - beta_channel = self._ota_update_params[ATTR_BETA] - update_data = self.device.status["sys"]["available_updates"] - new_version = update_data.get("stable", {"version": ""})["version"] - if beta_channel: - new_version = update_data.get(ATTR_BETA, {"version": ""})["version"] - - assert self.device.shelly - _LOGGER.info( - "Start OTA update of device %s from '%s' to '%s'", - self.name, - self.device.shelly["fw"], - new_version, - ) - result = None - try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - result = await self.device.trigger_ota_update(beta=beta_channel) - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error while perform ota update: %s", err) - - _LOGGER.debug("Result of OTA update call: %s", result) + await _async_do_ota_update() async def shutdown(self) -> None: """Shutdown the wrapper.""" From 2c89f169171f7b1cece01d97181697a5b9805a7b Mon Sep 17 00:00:00 2001 From: mib1185 Date: Sun, 7 Nov 2021 18:18:53 +0000 Subject: [PATCH 15/28] disable _ota_update_pending shen executed --- homeassistant/components/shelly/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index a378c4f218afce..00322fb0c182b0 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -324,6 +324,7 @@ def _async_device_updates_handler(self) -> None: # check if OTA update is scheduled if self._ota_update_pending: + self._ota_update_pending = False self.hass.async_create_task( self.async_trigger_ota_update(**self._ota_update_params) ) From f6af585aa20f108182ed91b1b057617cb2a35de2 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Sun, 7 Nov 2021 18:21:18 +0000 Subject: [PATCH 16/28] log warnings, instead of errors --- homeassistant/components/shelly/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 00322fb0c182b0..5d53bec5c931d4 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -464,23 +464,23 @@ async def _async_do_ota_update() -> None: _LOGGER.debug("OTA update service - update_data: %s", update_data) if self._ota_update_pending: - _LOGGER.error( + _LOGGER.warning( "OTA update already scheduled for sleeping device %s", self.name ) return if not update_data["has_update"] and not beta: - _LOGGER.error("No OTA update available for device %s", self.name) + _LOGGER.warning("No OTA update available for device %s", self.name) return if beta and not update_data.get("beta_version"): - _LOGGER.error( + _LOGGER.warning( "No OTA update on beta channel available for device %s", self.name ) return if update_data["status"] == "updating": - _LOGGER.error("OTA update already in progress for %s", self.name) + _LOGGER.warning("OTA update already in progress for %s", self.name) return if self.entry.data.get(CONF_SLEEP_PERIOD): @@ -761,11 +761,11 @@ async def _async_do_ota_update() -> None: _LOGGER.debug("OTA update service - update_data: %s", update_data) if not bool(update_data) or (not update_data.get("stable") and not beta): - _LOGGER.error("No OTA update available for device %s", self.name) + _LOGGER.warning("No OTA update available for device %s", self.name) return if beta and not update_data.get(ATTR_BETA): - _LOGGER.error( + _LOGGER.warning( "No OTA update on beta channel available for device %s", self.name ) return From 05352bd10a77e21f2b63dc6809f43f480a4b1267 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Sun, 7 Nov 2021 18:23:44 +0000 Subject: [PATCH 17/28] catch only expected errors --- homeassistant/components/shelly/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 5d53bec5c931d4..6f0789be923ac9 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -455,7 +455,7 @@ async def _async_do_ota_update() -> None: try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): result = await self.device.trigger_ota_update(beta=beta_channel) - except Exception as err: # pylint: disable=broad-except + except (asyncio.TimeoutError, OSError) as err: _LOGGER.exception("Error while perform ota update: %s", err) _LOGGER.debug("Result of OTA update call: %s", result) @@ -752,7 +752,7 @@ async def _async_do_ota_update() -> None: try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): result = await self.device.trigger_ota_update(beta=beta_channel) - except Exception as err: # pylint: disable=broad-except + except (asyncio.TimeoutError, OSError) as err: _LOGGER.exception("Error while perform ota update: %s", err) _LOGGER.debug("Result of OTA update call: %s", result) From 5e8c2ddf4f3e1e447fd0e982097eb465e5f52f48 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Tue, 9 Nov 2021 21:49:20 +0000 Subject: [PATCH 18/28] always create beta channel switch --- homeassistant/components/shelly/switch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 6bd48ec746fd05..5bf277896189e3 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -81,6 +81,9 @@ async def async_setup_rpc_entry( ) -> None: """Set up entities for RPC device.""" wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC] + + async_add_entities([ShellyOtaUpdateBetaChannelSwitch(wrapper, config_entry)]) + switch_key_ids = get_rpc_key_ids(wrapper.device.status, "switch") switch_ids = [] @@ -96,7 +99,6 @@ async def async_setup_rpc_entry( return async_add_entities(RpcRelaySwitch(wrapper, id_) for id_ in switch_ids) - async_add_entities([ShellyOtaUpdateBetaChannelSwitch(wrapper, config_entry)]) class BlockRelaySwitch(ShellyBlockEntity, SwitchEntity): From 523f8a65f6b30b24feadd52cda2afeb51e060150 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Tue, 9 Nov 2021 22:11:40 +0000 Subject: [PATCH 19/28] change button icon to mdi:package-up --- homeassistant/components/shelly/button.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index a1a746c1fae8c9..4210305a602b3f 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -50,7 +50,7 @@ async def async_setup_entry( class ShellyOtaUpdateButton(ButtonEntity): """Defines a Shelly OTA update button.""" - _attr_icon = "mdi:sync" + _attr_icon = "mdi:package-up" _attr_entity_category = ENTITY_CATEGORY_CONFIG def __init__( From 33946264d0cf0c163ff46743e55681cb23ff768a Mon Sep 17 00:00:00 2001 From: mib1185 Date: Tue, 9 Nov 2021 23:00:04 +0000 Subject: [PATCH 20/28] correct key from fw to fw_id --- homeassistant/components/shelly/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 6f0789be923ac9..27b0edbf5b2643 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -745,7 +745,7 @@ async def _async_do_ota_update() -> None: _LOGGER.info( "Start OTA update of device %s from '%s' to '%s'", self.name, - self.device.shelly["fw"], + self.device.shelly["fw_id"], new_version, ) result = None From a4e04790e4918ff8bd7cef4fbb1d653f4d8dfb8d Mon Sep 17 00:00:00 2001 From: mib1185 Date: Tue, 9 Nov 2021 23:05:47 +0000 Subject: [PATCH 21/28] use firmware_version property instead of low level --- homeassistant/components/shelly/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 27b0edbf5b2643..d0a29771efc926 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -448,7 +448,7 @@ async def _async_do_ota_update() -> None: _LOGGER.info( "Start OTA update of device %s from '%s' to '%s'", self.name, - update_data["old_version"], + self.device.firmware_version, new_version, ) result = None @@ -745,7 +745,7 @@ async def _async_do_ota_update() -> None: _LOGGER.info( "Start OTA update of device %s from '%s' to '%s'", self.name, - self.device.shelly["fw_id"], + self.device.firmware_version, new_version, ) result = None From 8fd649e0d77eaa132b3ee99bdcffb7cc83934ad9 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Tue, 9 Nov 2021 23:49:39 +0000 Subject: [PATCH 22/28] always create beta channel switch also on gen1 --- homeassistant/components/shelly/switch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 5bf277896189e3..356fd9339f0a25 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -48,6 +48,8 @@ async def async_setup_block_entry( """Set up entities for block device.""" wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] + async_add_entities([ShellyOtaUpdateBetaChannelSwitch(wrapper, config_entry)]) + # In roller mode the relay blocks exist but do not contain required info if ( wrapper.model in ["SHSW-21", "SHSW-25"] @@ -71,7 +73,6 @@ async def async_setup_block_entry( return async_add_entities(BlockRelaySwitch(wrapper, block) for block in relay_blocks) - async_add_entities([ShellyOtaUpdateBetaChannelSwitch(wrapper, config_entry)]) async def async_setup_rpc_entry( From df702e802751f7b9fd02aef086b89270b518e17e Mon Sep 17 00:00:00 2001 From: mib1185 Date: Thu, 11 Nov 2021 19:32:55 +0000 Subject: [PATCH 23/28] remove service, trigger ota direct in button --- homeassistant/components/shelly/__init__.py | 5 -- homeassistant/components/shelly/button.py | 21 +---- homeassistant/components/shelly/const.py | 2 - homeassistant/components/shelly/service.py | 36 -------- homeassistant/components/shelly/services.yaml | 19 ----- tests/components/shelly/test_button.py | 7 -- tests/components/shelly/test_service.py | 84 ------------------- 7 files changed, 4 insertions(+), 170 deletions(-) delete mode 100644 homeassistant/components/shelly/service.py delete mode 100644 homeassistant/components/shelly/services.yaml delete mode 100644 tests/components/shelly/test_service.py diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index d0a29771efc926..c5fd8079e7c977 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -57,7 +57,6 @@ SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, ) -from .service import async_services_setup from .utils import ( get_block_device_name, get_block_device_sleep_period, @@ -191,8 +190,6 @@ def _async_device_online(_: Any) -> None: _LOGGER.debug("Setting up offline block device %s", entry.title) await async_block_device_setup(hass, entry, device) - await async_services_setup(hass) - return True @@ -240,8 +237,6 @@ async def async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool hass.config_entries.async_setup_platforms(entry, RPC_PLATFORMS) - await async_services_setup(hass) - return True diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 4210305a602b3f..77dc622b2c53fa 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -5,7 +5,7 @@ from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_DEVICE_ID, ENTITY_CATEGORY_CONFIG +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo @@ -13,15 +13,7 @@ from homeassistant.util import slugify from . import BlockDeviceWrapper, RpcDeviceWrapper -from .const import ( - ATTR_BETA, - BLOCK, - CONF_OTA_BETA_CHANNEL, - DATA_CONFIG_ENTRY, - DOMAIN, - RPC, - SERVICE_OTA_UPDATE, -) +from .const import BLOCK, CONF_OTA_BETA_CHANNEL, DATA_CONFIG_ENTRY, DOMAIN, RPC from .utils import get_block_device_name, get_device_entry_gen, get_rpc_device_name @@ -74,11 +66,6 @@ def __init__( async def async_press(self) -> None: """Triggers the OTA update service.""" - await self.hass.services.async_call( - DOMAIN, - SERVICE_OTA_UPDATE, - { - ATTR_DEVICE_ID: self.wrapper.device_id, - ATTR_BETA: self.entry.options.get(CONF_OTA_BETA_CHANNEL), - }, + await self.wrapper.async_trigger_ota_update( + beta=bool(self.entry.options.get(CONF_OTA_BETA_CHANNEL)) ) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index ecd36ca62a63ba..2a5b67850cad96 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -10,8 +10,6 @@ DOMAIN: Final = "shelly" REST: Final = "rest" RPC: Final = "rpc" -SERVICE_OTA_UPDATE: Final = "ota_update" -SERVICES: Final = [SERVICE_OTA_UPDATE] CONF_COAP_PORT: Final = "coap_port" DEFAULT_COAP_PORT: Final = 5683 diff --git a/homeassistant/components/shelly/service.py b/homeassistant/components/shelly/service.py deleted file mode 100644 index 15aa127630647f..00000000000000 --- a/homeassistant/components/shelly/service.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Services for the Shelly integration.""" -from __future__ import annotations - -from homeassistant.const import ATTR_AREA_ID, ATTR_DEVICE_ID -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.service import async_extract_config_entry_ids - -from .const import ATTR_BETA, BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC, SERVICE_OTA_UPDATE -from .utils import get_device_entry_gen - - -async def async_services_setup(hass: HomeAssistant) -> None: - """Set up services.""" - - async def async_service_ota_update(call: ServiceCall) -> None: - """Trigger OTA update.""" - if not (call.data.get(ATTR_DEVICE_ID) or call.data.get(ATTR_AREA_ID)): - raise HomeAssistantError("No target selected for OTA update") - - beta_channel = bool(call.data.get(ATTR_BETA)) - - entry_ids = await async_extract_config_entry_ids(hass, call) - for entry_id in entry_ids: - entry = hass.config_entries.async_get_entry(entry_id) - if not (entry and entry.domain == DOMAIN): - continue - - if active_entry := hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(entry.entry_id): - if get_device_entry_gen(entry) == 2: - wrapper = active_entry.get(RPC) - else: - wrapper = active_entry.get(BLOCK) - await wrapper.async_trigger_ota_update(beta=beta_channel) - - hass.services.async_register(DOMAIN, SERVICE_OTA_UPDATE, async_service_ota_update) diff --git a/homeassistant/components/shelly/services.yaml b/homeassistant/components/shelly/services.yaml deleted file mode 100644 index 5b640c295c316e..00000000000000 --- a/homeassistant/components/shelly/services.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# shelly service descriptions. - -ota_update: - name: OTA Update - description: Trigger an over-the-air (OTA) update. - target: - device: - integration: shelly - entity: - integration: none - fields: - beta: - name: Beta - description: Run firmware update to beta version (if available) - required: false - default: false - example: true - selector: - boolean: diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index 83c68c3df8dd35..80392d38afa06d 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -1,7 +1,6 @@ """Tests for Shelly button platform.""" from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.button.const import SERVICE_PRESS -from homeassistant.components.shelly.service import async_services_setup from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_ON from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -20,7 +19,6 @@ async def test_block_button(hass: HomeAssistant, coap_wrapper): assert state assert state.state == STATE_UNKNOWN - await async_services_setup(hass) await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, @@ -43,8 +41,6 @@ async def test_block_button_beta(hass: HomeAssistant, coap_wrapper): ) await hass.async_block_till_done() - await async_services_setup(hass) - await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -74,7 +70,6 @@ async def test_rpc_button(hass: HomeAssistant, rpc_wrapper): assert state assert state.state == STATE_UNKNOWN - await async_services_setup(hass) await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, @@ -97,8 +92,6 @@ async def test_rpc_button_beta(hass: HomeAssistant, rpc_wrapper): ) await hass.async_block_till_done() - await async_services_setup(hass) - await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/shelly/test_service.py b/tests/components/shelly/test_service.py deleted file mode 100644 index 0caf9793139b5b..00000000000000 --- a/tests/components/shelly/test_service.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Tests for the Shelly services.""" -import pytest - -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.shelly.const import DOMAIN, SERVICE_OTA_UPDATE, SERVICES -from homeassistant.components.shelly.service import async_services_setup -from homeassistant.const import ATTR_DEVICE_ID -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import ( - async_entries_for_config_entry, - async_get, -) - - -async def test_services_registered(hass): - """Test if all services are registered.""" - await async_services_setup(hass) - for service in SERVICES: - assert hass.services.has_service(DOMAIN, service) - - -async def test_service_error(hass): - """Test for errors in service call.""" - await async_services_setup(hass) - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - DOMAIN, - SERVICE_OTA_UPDATE, - {}, - blocking=True, - ) - await hass.async_block_till_done() - - -async def test_service_ota_coap_device(hass, coap_wrapper): - """Test OTA update service with block device.""" - assert coap_wrapper - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - coap_wrapper.entry, BINARY_SENSOR_DOMAIN - ) - ) - await hass.async_block_till_done() - - dev_reg = async_get(hass) - devices = async_entries_for_config_entry(dev_reg, coap_wrapper.entry.entry_id) - assert devices - assert devices[0] - - await async_services_setup(hass) - await hass.services.async_call( - DOMAIN, - SERVICE_OTA_UPDATE, - {ATTR_DEVICE_ID: devices[0].id}, - blocking=True, - ) - await hass.async_block_till_done() - coap_wrapper.device.trigger_ota_update.assert_called_once() - - -async def test_service_ota_rpc_device(hass, rpc_wrapper): - """Test OTA update service with rpc device.""" - assert rpc_wrapper - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - rpc_wrapper.entry, BINARY_SENSOR_DOMAIN - ) - ) - await hass.async_block_till_done() - - dev_reg = async_get(hass) - devices = async_entries_for_config_entry(dev_reg, rpc_wrapper.entry.entry_id) - assert devices - assert devices[0] - - await async_services_setup(hass) - await hass.services.async_call( - DOMAIN, - SERVICE_OTA_UPDATE, - {ATTR_DEVICE_ID: devices[0].id}, - blocking=True, - ) - await hass.async_block_till_done() - rpc_wrapper.device.trigger_ota_update.assert_called_once() From bbd7e7618642f2e13c102e57b2b9f7edaf34d5b6 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Thu, 11 Nov 2021 21:15:34 +0000 Subject: [PATCH 24/28] remove support for battery powered devices --- homeassistant/components/shelly/__init__.py | 111 ++++++-------------- 1 file changed, 35 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index c5fd8079e7c977..8499dc974e559f 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -73,7 +73,7 @@ "sensor", "switch", ] -BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "button", "sensor"] +BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "sensor"] RPC_PLATFORMS: Final = ["binary_sensor", "button", "light", "sensor", "switch"] _LOGGER: Final = logging.getLogger(__name__) @@ -281,9 +281,6 @@ def __init__( self._last_mode: str | None = None self._last_effect: int | None = None - self._ota_update_pending = False - self._ota_update_params = {ATTR_BETA: False} - entry.async_on_unload( self.async_add_listener(self._async_device_updates_handler) ) @@ -317,13 +314,6 @@ def _async_device_updates_handler(self) -> None: break - # check if OTA update is scheduled - if self._ota_update_pending: - self._ota_update_pending = False - self.hass.async_create_task( - self.async_trigger_ota_update(**self._ota_update_params) - ) - # Check for input events and config change cfg_changed = 0 for block in self.device.blocks: @@ -431,39 +421,9 @@ def async_setup(self) -> None: async def async_trigger_ota_update(self, beta: bool = False) -> None: """Trigger or schedule an ota update.""" - - async def _async_do_ota_update() -> None: - """Perform an ota update.""" - beta_channel = self._ota_update_params[ATTR_BETA] - update_data = self.device.status["update"] - new_version = update_data["new_version"] - if beta_channel: - new_version = update_data["beta_version"] - - _LOGGER.info( - "Start OTA update of device %s from '%s' to '%s'", - self.name, - self.device.firmware_version, - new_version, - ) - result = None - try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - result = await self.device.trigger_ota_update(beta=beta_channel) - except (asyncio.TimeoutError, OSError) as err: - _LOGGER.exception("Error while perform ota update: %s", err) - - _LOGGER.debug("Result of OTA update call: %s", result) - update_data = self.device.status["update"] _LOGGER.debug("OTA update service - update_data: %s", update_data) - if self._ota_update_pending: - _LOGGER.warning( - "OTA update already scheduled for sleeping device %s", self.name - ) - return - if not update_data["has_update"] and not beta: _LOGGER.warning("No OTA update available for device %s", self.name) return @@ -478,13 +438,21 @@ async def _async_do_ota_update() -> None: _LOGGER.warning("OTA update already in progress for %s", self.name) return - if self.entry.data.get(CONF_SLEEP_PERIOD): - self._ota_update_pending = True - self._ota_update_params = {ATTR_BETA: beta} - _LOGGER.info("OTA update scheduled for sleeping device %s", self.name) - else: - self._ota_update_params = {ATTR_BETA: beta} - await _async_do_ota_update() + new_version = update_data["new_version"] + if beta: + new_version = update_data["beta_version"] + _LOGGER.info( + "Start OTA update of device %s from '%s' to '%s'", + self.name, + self.device.firmware_version, + new_version, + ) + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + result = await self.device.trigger_ota_update(beta=beta) + except (asyncio.TimeoutError, OSError) as err: + _LOGGER.exception("Error while perform ota update: %s", err) + _LOGGER.debug("Result of OTA update call: %s", result) def shutdown(self) -> None: """Shutdown the wrapper.""" @@ -624,8 +592,6 @@ def __init__( self.entry = entry self.device = device - self._ota_update_params = {ATTR_BETA: False} - self._debounced_reload = Debouncer( hass, _LOGGER, @@ -728,30 +694,6 @@ def async_setup(self) -> None: async def async_trigger_ota_update(self, beta: bool = False) -> None: """Trigger an ota update.""" - async def _async_do_ota_update() -> None: - """Perform an ota update.""" - beta_channel = self._ota_update_params[ATTR_BETA] - update_data = self.device.status["sys"]["available_updates"] - new_version = update_data.get("stable", {"version": ""})["version"] - if beta_channel: - new_version = update_data.get(ATTR_BETA, {"version": ""})["version"] - - assert self.device.shelly - _LOGGER.info( - "Start OTA update of device %s from '%s' to '%s'", - self.name, - self.device.firmware_version, - new_version, - ) - result = None - try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - result = await self.device.trigger_ota_update(beta=beta_channel) - except (asyncio.TimeoutError, OSError) as err: - _LOGGER.exception("Error while perform ota update: %s", err) - - _LOGGER.debug("Result of OTA update call: %s", result) - update_data = self.device.status["sys"]["available_updates"] _LOGGER.debug("OTA update service - update_data: %s", update_data) @@ -765,8 +707,25 @@ async def _async_do_ota_update() -> None: ) return - self._ota_update_params = {ATTR_BETA: beta} - await _async_do_ota_update() + new_version = update_data.get("stable", {"version": ""})["version"] + if beta: + new_version = update_data.get(ATTR_BETA, {"version": ""})["version"] + + assert self.device.shelly + _LOGGER.info( + "Start OTA update of device %s from '%s' to '%s'", + self.name, + self.device.firmware_version, + new_version, + ) + result = None + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + result = await self.device.trigger_ota_update(beta=beta) + except (asyncio.TimeoutError, OSError) as err: + _LOGGER.exception("Error while perform ota update: %s", err) + + _LOGGER.debug("Result of OTA update call: %s", result) async def shutdown(self) -> None: """Shutdown the wrapper.""" From 9ea6b15c4f2ff8aabed55e5d1065569f530d4305 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Sat, 13 Nov 2021 22:52:45 +0000 Subject: [PATCH 25/28] remove virtual switch --- homeassistant/components/shelly/switch.py | 55 +---------------------- 1 file changed, 1 insertion(+), 54 deletions(-) diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 356fd9339f0a25..9114587a910fd7 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -7,21 +7,15 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import slugify from . import BlockDeviceWrapper, RpcDeviceWrapper -from .const import BLOCK, CONF_OTA_BETA_CHANNEL, DATA_CONFIG_ENTRY, DOMAIN, RPC +from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC from .entity import ShellyBlockEntity, ShellyRpcEntity from .utils import ( async_remove_shelly_entity, - get_block_device_name, get_device_entry_gen, - get_rpc_device_name, get_rpc_key_ids, is_block_channel_type_light, is_rpc_channel_type_light, @@ -48,8 +42,6 @@ async def async_setup_block_entry( """Set up entities for block device.""" wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] - async_add_entities([ShellyOtaUpdateBetaChannelSwitch(wrapper, config_entry)]) - # In roller mode the relay blocks exist but do not contain required info if ( wrapper.model in ["SHSW-21", "SHSW-25"] @@ -83,8 +75,6 @@ async def async_setup_rpc_entry( """Set up entities for RPC device.""" wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC] - async_add_entities([ShellyOtaUpdateBetaChannelSwitch(wrapper, config_entry)]) - switch_key_ids = get_rpc_key_ids(wrapper.device.status, "switch") switch_ids = [] @@ -155,46 +145,3 @@ async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None: """Turn off relay.""" await self.call_rpc("Switch.Set", {"id": self._id, "on": False}) - - -class ShellyOtaUpdateBetaChannelSwitch(SwitchEntity): - """Defines a Shelly OTA update beta channel switch.""" - - _attr_icon = "mdi:flask-outline" - _attr_entity_category = ENTITY_CATEGORY_CONFIG - - def __init__( - self, wrapper: RpcDeviceWrapper | BlockDeviceWrapper, entry: ConfigEntry - ) -> None: - """Initialize Shelly OTA update beta channel switch.""" - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, wrapper.mac)} - ) - - if isinstance(wrapper, RpcDeviceWrapper): - device_name = get_rpc_device_name(wrapper.device) - else: - device_name = get_block_device_name(wrapper.device) - - self._attr_name = f"{device_name} OTA Update Beta channel" - self._attr_unique_id = slugify(self._attr_name) - - self.entry = entry - self.wrapper = wrapper - - @property - def is_on(self) -> bool: - """Return true if device is on.""" - return bool(self.entry.options.get(CONF_OTA_BETA_CHANNEL)) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the device on.""" - self.hass.config_entries.async_update_entry( - self.entry, options={**self.entry.options, CONF_OTA_BETA_CHANNEL: True} - ) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the device off.""" - self.hass.config_entries.async_update_entry( - self.entry, options={**self.entry.options, CONF_OTA_BETA_CHANNEL: False} - ) From e0b34ef4e0763a3a3d727e8ae81480aa9ecf11f7 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Sat, 13 Nov 2021 22:53:15 +0000 Subject: [PATCH 26/28] add additional button for beta channel updates --- homeassistant/components/shelly/button.py | 49 ++++++++++++++++++----- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 77dc622b2c53fa..ad3223b47f05a1 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -13,7 +13,7 @@ from homeassistant.util import slugify from . import BlockDeviceWrapper, RpcDeviceWrapper -from .const import BLOCK, CONF_OTA_BETA_CHANNEL, DATA_CONFIG_ENTRY, DOMAIN, RPC +from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC from .utils import get_block_device_name, get_device_entry_gen, get_rpc_device_name @@ -36,17 +36,26 @@ async def async_setup_entry( wrapper = cast(BlockDeviceWrapper, block_wrapper) if wrapper is not None: - async_add_entities([ShellyOtaUpdateButton(wrapper, config_entry)]) + async_add_entities( + [ + ShellyOtaUpdateStableButton(wrapper, config_entry), + ShellyOtaUpdateBetaButton(wrapper, config_entry), + ] + ) -class ShellyOtaUpdateButton(ButtonEntity): - """Defines a Shelly OTA update button.""" +class ShellyOtaUpdateBaseButton(ButtonEntity): + """Defines a Shelly OTA update base button.""" - _attr_icon = "mdi:package-up" _attr_entity_category = ENTITY_CATEGORY_CONFIG def __init__( - self, wrapper: RpcDeviceWrapper | BlockDeviceWrapper, entry: ConfigEntry + self, + wrapper: RpcDeviceWrapper | BlockDeviceWrapper, + entry: ConfigEntry, + name: str, + beta_channel: bool, + icon: str, ) -> None: """Initialize Shelly OTA update button.""" self._attr_device_info = DeviceInfo( @@ -58,14 +67,34 @@ def __init__( else: device_name = get_block_device_name(wrapper.device) - self._attr_name = f"{device_name} OTA Update" + self._attr_name = f"{device_name} {name}" self._attr_unique_id = slugify(self._attr_name) + self._attr_icon = icon + self.beta_channel = beta_channel self.entry = entry self.wrapper = wrapper async def async_press(self) -> None: """Triggers the OTA update service.""" - await self.wrapper.async_trigger_ota_update( - beta=bool(self.entry.options.get(CONF_OTA_BETA_CHANNEL)) - ) + await self.wrapper.async_trigger_ota_update(beta=self.beta_channel) + + +class ShellyOtaUpdateStableButton(ShellyOtaUpdateBaseButton): + """Defines a Shelly OTA update stable channel button.""" + + def __init__( + self, wrapper: RpcDeviceWrapper | BlockDeviceWrapper, entry: ConfigEntry + ) -> None: + """Initialize Shelly OTA update button.""" + super().__init__(wrapper, entry, "OTA Update", False, "mdi:package-up") + + +class ShellyOtaUpdateBetaButton(ShellyOtaUpdateBaseButton): + """Defines a Shelly OTA update beta channel button.""" + + def __init__( + self, wrapper: RpcDeviceWrapper | BlockDeviceWrapper, entry: ConfigEntry + ) -> None: + """Initialize Shelly OTA update button.""" + super().__init__(wrapper, entry, "OTA Update Beta", True, "mdi:flask-outline") From e5ce13e488a3fc939d315b5925da0967de700cee Mon Sep 17 00:00:00 2001 From: mib1185 Date: Sat, 13 Nov 2021 22:53:22 +0000 Subject: [PATCH 27/28] adjust tests --- tests/components/shelly/test_button.py | 97 ++++++++------------------ tests/components/shelly/test_switch.py | 64 ----------------- 2 files changed, 30 insertions(+), 131 deletions(-) diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index 80392d38afa06d..b7536be1975fcf 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -1,12 +1,22 @@ """Tests for Shelly button platform.""" +import pytest + from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.button.const import SERVICE_PRESS -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_ON from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant -async def test_block_button(hass: HomeAssistant, coap_wrapper): +@pytest.mark.parametrize( + "name,beta_channel", + [ + ("button.test_name_ota_update", False), + ("button.test_name_ota_update_beta", True), + ], +) +async def test_block_button( + hass: HomeAssistant, name: str, beta_channel: bool, coap_wrapper +): """Test block device OTA button.""" assert coap_wrapper @@ -15,49 +25,30 @@ async def test_block_button(hass: HomeAssistant, coap_wrapper): ) await hass.async_block_till_done() - state = hass.states.get("button.test_name_ota_update") + state = hass.states.get(name) assert state assert state.state == STATE_UNKNOWN await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.test_name_ota_update"}, - blocking=True, - ) - await hass.async_block_till_done() - coap_wrapper.device.trigger_ota_update.assert_called_once_with(beta=False) - - -async def test_block_button_beta(hass: HomeAssistant, coap_wrapper): - """Test block device OTA button with beta channel enabled.""" - assert coap_wrapper - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, BUTTON_DOMAIN) - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) - ) - await hass.async_block_till_done() - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.test_name_ota_update_beta_channel"}, - blocking=True, - ) - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.test_name_ota_update"}, + {ATTR_ENTITY_ID: name}, blocking=True, ) await hass.async_block_till_done() - coap_wrapper.device.trigger_ota_update.assert_called_once_with(beta=True) - - -async def test_rpc_button(hass: HomeAssistant, rpc_wrapper): + coap_wrapper.device.trigger_ota_update.assert_called_once_with(beta=beta_channel) + + +@pytest.mark.parametrize( + "name,beta_channel", + [ + ("button.test_name_ota_update", False), + ("button.test_name_ota_update_beta", True), + ], +) +async def test_rpc_button( + hass: HomeAssistant, name: str, beta_channel: bool, rpc_wrapper +): """Test rpc device OTA button.""" assert rpc_wrapper @@ -66,43 +57,15 @@ async def test_rpc_button(hass: HomeAssistant, rpc_wrapper): ) await hass.async_block_till_done() - state = hass.states.get("button.test_name_ota_update") + state = hass.states.get(name) assert state assert state.state == STATE_UNKNOWN await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.test_name_ota_update"}, - blocking=True, - ) - await hass.async_block_till_done() - rpc_wrapper.device.trigger_ota_update.assert_called_once_with(beta=False) - - -async def test_rpc_button_beta(hass: HomeAssistant, rpc_wrapper): - """Test rpc device OTA button with beta channel enabled.""" - assert rpc_wrapper - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, BUTTON_DOMAIN) - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, SWITCH_DOMAIN) - ) - await hass.async_block_till_done() - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.test_name_ota_update_beta_channel"}, - blocking=True, - ) - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.test_name_ota_update"}, + {ATTR_ENTITY_ID: name}, blocking=True, ) await hass.async_block_till_done() - rpc_wrapper.device.trigger_ota_update.assert_called_once_with(beta=True) + rpc_wrapper.device.trigger_ota_update.assert_called_once_with(beta=beta_channel) diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index ce7f1dbbd339f8..fc61102507b1c1 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -85,38 +85,6 @@ async def test_block_device_mode_roller(hass, coap_wrapper, monkeypatch): assert hass.states.get("switch.test_name_channel_1") is None -async def test_block_ota_beta_switch(hass, coap_wrapper): - """Test block device OTA update beta channel switch.""" - assert coap_wrapper - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) - ) - await hass.async_block_till_done() - - state = hass.states.get("switch.test_name_ota_update_beta_channel") - assert state - assert state.state == STATE_OFF - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.test_name_ota_update_beta_channel"}, - blocking=True, - ) - assert hass.states.get("switch.test_name_ota_update_beta_channel").state == STATE_ON - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_name_ota_update_beta_channel"}, - blocking=True, - ) - assert ( - hass.states.get("switch.test_name_ota_update_beta_channel").state == STATE_OFF - ) - - async def test_block_device_app_type_light(hass, coap_wrapper, monkeypatch): """Test block device in app type set to light mode.""" assert coap_wrapper @@ -173,35 +141,3 @@ async def test_rpc_device_switch_type_lights_mode(hass, rpc_wrapper, monkeypatch ) await hass.async_block_till_done() assert hass.states.get("switch.test_switch_0") is None - - -async def test_rpc_ota_beta_switch(hass, rpc_wrapper): - """Test rpc device OTA update beta channel switch.""" - assert rpc_wrapper - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, SWITCH_DOMAIN) - ) - await hass.async_block_till_done() - - state = hass.states.get("switch.test_name_ota_update_beta_channel") - assert state - assert state.state == STATE_OFF - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.test_name_ota_update_beta_channel"}, - blocking=True, - ) - assert hass.states.get("switch.test_name_ota_update_beta_channel").state == STATE_ON - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_name_ota_update_beta_channel"}, - blocking=True, - ) - assert ( - hass.states.get("switch.test_name_ota_update_beta_channel").state == STATE_OFF - ) From 8a49e60911527101f3cb7d8200a9c20ef94d272f Mon Sep 17 00:00:00 2001 From: mib1185 Date: Tue, 16 Nov 2021 21:08:00 +0000 Subject: [PATCH 28/28] disable beta button by default --- homeassistant/components/shelly/button.py | 1 + tests/components/shelly/test_button.py | 55 +++++++++++------------ 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index ad3223b47f05a1..7890eefd30c682 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -98,3 +98,4 @@ def __init__( ) -> None: """Initialize Shelly OTA update button.""" super().__init__(wrapper, entry, "OTA Update Beta", True, "mdi:flask-outline") + self._attr_entity_registry_enabled_default = False diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index b7536be1975fcf..5ceed08b9d9794 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -1,22 +1,12 @@ """Tests for Shelly button platform.""" -import pytest - from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.button.const import SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import async_get -@pytest.mark.parametrize( - "name,beta_channel", - [ - ("button.test_name_ota_update", False), - ("button.test_name_ota_update_beta", True), - ], -) -async def test_block_button( - hass: HomeAssistant, name: str, beta_channel: bool, coap_wrapper -): +async def test_block_button(hass: HomeAssistant, coap_wrapper): """Test block device OTA button.""" assert coap_wrapper @@ -25,30 +15,30 @@ async def test_block_button( ) await hass.async_block_till_done() - state = hass.states.get(name) + # stable channel button + state = hass.states.get("button.test_name_ota_update") assert state assert state.state == STATE_UNKNOWN await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: name}, + {ATTR_ENTITY_ID: "button.test_name_ota_update"}, blocking=True, ) await hass.async_block_till_done() - coap_wrapper.device.trigger_ota_update.assert_called_once_with(beta=beta_channel) + coap_wrapper.device.trigger_ota_update.assert_called_once_with(beta=False) + + # beta channel button + entity_registry = async_get(hass) + entry = entity_registry.async_get("button.test_name_ota_update_beta") + state = hass.states.get("button.test_name_ota_update_beta") + + assert entry + assert state is None -@pytest.mark.parametrize( - "name,beta_channel", - [ - ("button.test_name_ota_update", False), - ("button.test_name_ota_update_beta", True), - ], -) -async def test_rpc_button( - hass: HomeAssistant, name: str, beta_channel: bool, rpc_wrapper -): +async def test_rpc_button(hass: HomeAssistant, rpc_wrapper): """Test rpc device OTA button.""" assert rpc_wrapper @@ -57,15 +47,24 @@ async def test_rpc_button( ) await hass.async_block_till_done() - state = hass.states.get(name) + # stable channel button + state = hass.states.get("button.test_name_ota_update") assert state assert state.state == STATE_UNKNOWN await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: name}, + {ATTR_ENTITY_ID: "button.test_name_ota_update"}, blocking=True, ) await hass.async_block_till_done() - rpc_wrapper.device.trigger_ota_update.assert_called_once_with(beta=beta_channel) + rpc_wrapper.device.trigger_ota_update.assert_called_once_with(beta=False) + + # beta channel button + entity_registry = async_get(hass) + entry = entity_registry.async_get("button.test_name_ota_update_beta") + state = hass.states.get("button.test_name_ota_update_beta") + + assert entry + assert state is None