From 6e453ae471b9aa6eaf6f29634292bd6cf7eb79e1 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 24 Oct 2022 14:49:18 +1100 Subject: [PATCH] Add device-specific diagnostics to the LIFX integration (#79964) --- homeassistant/components/lifx/const.py | 1 + homeassistant/components/lifx/coordinator.py | 38 ++ homeassistant/components/lifx/diagnostics.py | 28 ++ tests/components/lifx/__init__.py | 3 + tests/components/lifx/test_diagnostics.py | 385 +++++++++++++++++++ 5 files changed, 455 insertions(+) create mode 100644 homeassistant/components/lifx/diagnostics.py create mode 100644 tests/components/lifx/test_diagnostics.py diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py index a81cd7d59be..1502c51204b 100644 --- a/homeassistant/components/lifx/const.py +++ b/homeassistant/components/lifx/const.py @@ -12,6 +12,7 @@ MESSAGE_RETRIES = 5 OVERALL_TIMEOUT = 9 UNAVAILABLE_GRACE = 90 +CONF_LABEL = "label" CONF_SERIAL = "serial" IDENTIFY_WAVEFORM = { diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index b89fefb35fd..2ec3a6d3745 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -117,6 +117,44 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): """Return the current infrared brightness as a string.""" return infrared_brightness_value_to_option(self.device.infrared_brightness) + async def diagnostics(self) -> dict[str, Any]: + """Return diagnostic information about the device.""" + features = lifx_features(self.device) + device_data = { + "firmware": self.device.host_firmware_version, + "vendor": self.device.vendor, + "product_id": self.device.product, + "features": features, + "hue": self.device.color[0], + "saturation": self.device.color[1], + "brightness": self.device.color[2], + "kelvin": self.device.color[3], + "power": self.device.power_level, + } + + if features["multizone"] is True: + zones = {"count": self.device.zones_count, "state": {}} + for index, zone_color in enumerate(self.device.color_zones): + zones["state"][index] = { + "hue": zone_color[0], + "saturation": zone_color[1], + "brightness": zone_color[2], + "kelvin": zone_color[3], + } + device_data["zones"] = zones + + if features["hev"] is True: + device_data["hev"] = { + "hev_cycle": self.device.hev_cycle, + "hev_config": self.device.hev_cycle_configuration, + "last_result": self.device.last_hev_cycle_result, + } + + if features["infrared"] is True: + device_data["infrared"] = {"brightness": self.device.infrared_brightness} + + return device_data + def async_get_entity_id(self, platform: Platform, key: str) -> str | None: """Return the entity_id from the platform and key provided.""" ent_reg = er.async_get(self.hass) diff --git a/homeassistant/components/lifx/diagnostics.py b/homeassistant/components/lifx/diagnostics.py new file mode 100644 index 00000000000..abe13cd1a50 --- /dev/null +++ b/homeassistant/components/lifx/diagnostics.py @@ -0,0 +1,28 @@ +"""Diagnostics support for LIFX.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS, CONF_MAC +from homeassistant.core import HomeAssistant + +from .const import CONF_LABEL, DOMAIN +from .coordinator import LIFXUpdateCoordinator + +TO_REDACT = [CONF_LABEL, CONF_HOST, CONF_IP_ADDRESS, CONF_MAC] + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a LIFX config entry.""" + coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + return { + "entry": { + "title": entry.title, + "data": async_redact_data(dict(entry.data), TO_REDACT), + }, + "data": async_redact_data(await coordinator.diagnostics(), TO_REDACT), + } diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index df3c41ccaca..543f2f7d7a2 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -123,12 +123,15 @@ def _mocked_clean_bulb() -> Light: bulb = _mocked_bulb() bulb.get_hev_cycle = MockLifxCommand(bulb) bulb.set_hev_cycle = MockLifxCommand(bulb) + bulb.get_hev_configuration = MockLifxCommand(bulb) + bulb.get_last_hev_cycle_result = MockLifxCommand(bulb) bulb.hev_cycle_configuration = {"duration": 7200, "indication": False} bulb.hev_cycle = { "duration": 7200, "remaining": 30, "last_power": False, } + bulb.last_hev_cycle_result = 0 bulb.product = 90 return bulb diff --git a/tests/components/lifx/test_diagnostics.py b/tests/components/lifx/test_diagnostics.py new file mode 100644 index 00000000000..4eccd19634d --- /dev/null +++ b/tests/components/lifx/test_diagnostics.py @@ -0,0 +1,385 @@ +"""Test LIFX diagnostics.""" +from homeassistant.components import lifx +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + MAC_ADDRESS, + _mocked_bulb, + _mocked_clean_bulb, + _mocked_infrared_bulb, + _mocked_light_strip, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_bulb_diagnostics(hass: HomeAssistant, hass_client) -> None: + """Test diagnostics for a standard bulb.""" + config_entry = MockConfigEntry( + domain=lifx.DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert diag == { + "data": { + "brightness": 3, + "features": { + "buttons": False, + "chain": False, + "color": True, + "extended_multizone": False, + "hev": False, + "infrared": False, + "matrix": False, + "max_kelvin": 9000, + "min_kelvin": 2500, + "multizone": False, + "relays": False, + }, + "firmware": "3.00", + "hue": 1, + "kelvin": 4, + "power": 0, + "product_id": 1, + "saturation": 2, + "vendor": None, + }, + "entry": {"data": {"host": "**REDACTED**"}, "title": "My Bulb"}, + } + + +async def test_clean_bulb_diagnostics(hass: HomeAssistant, hass_client) -> None: + """Test diagnostics for a standard bulb.""" + config_entry = MockConfigEntry( + domain=lifx.DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_clean_bulb() + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert diag == { + "data": { + "brightness": 3, + "features": { + "buttons": False, + "chain": False, + "color": True, + "extended_multizone": False, + "hev": True, + "infrared": False, + "matrix": False, + "max_kelvin": 9000, + "min_kelvin": 1500, + "multizone": False, + "relays": False, + }, + "firmware": "3.00", + "hev": { + "hev_config": {"duration": 7200, "indication": False}, + "hev_cycle": {"duration": 7200, "last_power": False, "remaining": 30}, + "last_result": 0, + }, + "hue": 1, + "kelvin": 4, + "power": 0, + "product_id": 90, + "saturation": 2, + "vendor": None, + }, + "entry": {"data": {"host": "**REDACTED**"}, "title": "My Bulb"}, + } + + +async def test_infrared_bulb_diagnostics(hass: HomeAssistant, hass_client) -> None: + """Test diagnostics for a standard bulb.""" + config_entry = MockConfigEntry( + domain=lifx.DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_infrared_bulb() + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert diag == { + "data": { + "brightness": 3, + "features": { + "buttons": False, + "chain": False, + "color": True, + "extended_multizone": False, + "hev": False, + "infrared": True, + "matrix": False, + "max_kelvin": 9000, + "min_kelvin": 1500, + "multizone": False, + "relays": False, + }, + "firmware": "3.00", + "hue": 1, + "infrared": {"brightness": 65535}, + "kelvin": 4, + "power": 0, + "product_id": 29, + "saturation": 2, + "vendor": None, + }, + "entry": {"data": {"host": "**REDACTED**"}, "title": "My Bulb"}, + } + + +async def test_legacy_multizone_bulb_diagnostics( + hass: HomeAssistant, hass_client +) -> None: + """Test diagnostics for a standard bulb.""" + config_entry = MockConfigEntry( + domain=lifx.DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_light_strip() + bulb.zones_count = 8 + bulb.color_zones = [ + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert diag == { + "data": { + "brightness": 3, + "features": { + "buttons": False, + "chain": False, + "color": True, + "extended_multizone": False, + "hev": False, + "infrared": False, + "matrix": False, + "max_kelvin": 9000, + "min_kelvin": 2500, + "multizone": True, + "relays": False, + }, + "firmware": "3.00", + "hue": 1, + "kelvin": 4, + "power": 0, + "product_id": 31, + "saturation": 2, + "vendor": None, + "zones": { + "count": 8, + "state": { + "0": { + "brightness": 65535, + "hue": 54612, + "kelvin": 3500, + "saturation": 65535, + }, + "1": { + "brightness": 65535, + "hue": 54612, + "kelvin": 3500, + "saturation": 65535, + }, + "2": { + "brightness": 65535, + "hue": 54612, + "kelvin": 3500, + "saturation": 65535, + }, + "3": { + "brightness": 65535, + "hue": 54612, + "kelvin": 3500, + "saturation": 65535, + }, + "4": { + "brightness": 65535, + "hue": 46420, + "kelvin": 3500, + "saturation": 65535, + }, + "5": { + "brightness": 65535, + "hue": 46420, + "kelvin": 3500, + "saturation": 65535, + }, + "6": { + "brightness": 65535, + "hue": 46420, + "kelvin": 3500, + "saturation": 65535, + }, + "7": { + "brightness": 65535, + "hue": 46420, + "kelvin": 3500, + "saturation": 65535, + }, + }, + }, + }, + "entry": {"data": {"host": "**REDACTED**"}, "title": "My Bulb"}, + } + + +async def test_multizone_bulb_diagnostics(hass: HomeAssistant, hass_client) -> None: + """Test diagnostics for a standard bulb.""" + config_entry = MockConfigEntry( + domain=lifx.DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_light_strip() + bulb.product = 38 + bulb.zones_count = 8 + bulb.color_zones = [ + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert diag == { + "data": { + "brightness": 3, + "features": { + "buttons": False, + "chain": False, + "color": True, + "extended_multizone": True, + "hev": False, + "infrared": False, + "matrix": False, + "max_kelvin": 9000, + "min_ext_mz_firmware": 1532997580, + "min_ext_mz_firmware_components": [2, 77], + "min_kelvin": 1500, + "multizone": True, + "relays": False, + }, + "firmware": "3.00", + "hue": 1, + "kelvin": 4, + "power": 0, + "product_id": 38, + "saturation": 2, + "vendor": None, + "zones": { + "count": 8, + "state": { + "0": { + "brightness": 65535, + "hue": 54612, + "kelvin": 3500, + "saturation": 65535, + }, + "1": { + "brightness": 65535, + "hue": 54612, + "kelvin": 3500, + "saturation": 65535, + }, + "2": { + "brightness": 65535, + "hue": 54612, + "kelvin": 3500, + "saturation": 65535, + }, + "3": { + "brightness": 65535, + "hue": 54612, + "kelvin": 3500, + "saturation": 65535, + }, + "4": { + "brightness": 65535, + "hue": 46420, + "kelvin": 3500, + "saturation": 65535, + }, + "5": { + "brightness": 65535, + "hue": 46420, + "kelvin": 3500, + "saturation": 65535, + }, + "6": { + "brightness": 65535, + "hue": 46420, + "kelvin": 3500, + "saturation": 65535, + }, + "7": { + "brightness": 65535, + "hue": 46420, + "kelvin": 3500, + "saturation": 65535, + }, + }, + }, + }, + "entry": {"data": {"host": "**REDACTED**"}, "title": "My Bulb"}, + }