From 176f03d4ac6d30b60a7ffe169ed940fe2e8fdb9f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 23 Jan 2022 11:31:01 +0100 Subject: [PATCH] Allow toggles (switches) state to be None (#64621) --- homeassistant/components/esphome/fan.py | 2 +- homeassistant/components/esphome/light.py | 2 +- homeassistant/components/esphome/switch.py | 2 +- homeassistant/components/freedompro/fan.py | 4 +++- homeassistant/components/fritz/switch.py | 4 ++-- homeassistant/components/modbus/fan.py | 2 +- homeassistant/components/switchbot/switch.py | 2 +- homeassistant/components/zwave_js/fan.py | 2 +- homeassistant/components/zwave_js/switch.py | 4 ++-- homeassistant/helpers/entity.py | 10 ++++++---- tests/components/blebox/test_light.py | 12 +++++++++--- tests/components/blebox/test_switch.py | 5 +++-- tests/components/rfxtrx/test_light.py | 9 +++++---- tests/components/rfxtrx/test_switch.py | 17 +++++++++-------- 14 files changed, 45 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 6abce0914cb..5ae4a47c52e 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -117,7 +117,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): ) @esphome_state_property - def is_on(self) -> bool | None: # type: ignore[override] + def is_on(self) -> bool | None: """Return true if the entity is on.""" return self._state.state diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index eb5e258f079..44649e9e219 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -144,7 +144,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): return self._api_version >= APIVersion(1, 6) @esphome_state_property - def is_on(self) -> bool | None: # type: ignore[override] + def is_on(self) -> bool | None: """Return true if the light is on.""" return self._state.state diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index a8f0febf5b0..952cb96fcd8 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -41,7 +41,7 @@ class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): return self._static_info.assumed_state @esphome_state_property - def is_on(self) -> bool | None: # type: ignore[override] + def is_on(self) -> bool | None: """Return true if the switch is on.""" return self._state.state diff --git a/homeassistant/components/freedompro/fan.py b/homeassistant/components/freedompro/fan.py index 38d777284e6..fe8ccc6d28d 100644 --- a/homeassistant/components/freedompro/fan.py +++ b/homeassistant/components/freedompro/fan.py @@ -1,4 +1,6 @@ """Support for Freedompro fan.""" +from __future__ import annotations + import json from pyfreedompro import put_state @@ -51,7 +53,7 @@ class FreedomproFan(CoordinatorEntity, FanEntity): self._attr_percentage = 0 @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return True if entity is on.""" return self._attr_is_on diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 0b7a33692e6..2d6af382e44 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -364,6 +364,8 @@ async def async_setup_entry( class FritzBoxBaseSwitch(FritzBoxBaseEntity): """Fritz switch base class.""" + _attr_is_on: bool | None = False + def __init__( self, avm_wrapper: AvmWrapper, @@ -386,8 +388,6 @@ class FritzBoxBaseSwitch(FritzBoxBaseEntity): self._attributes: dict[str, str] = {} self._is_available = True - self._attr_is_on = False - @property def name(self) -> str: """Return name.""" diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py index 62532cc93ac..afa26ac1638 100644 --- a/homeassistant/components/modbus/fan.py +++ b/homeassistant/components/modbus/fan.py @@ -48,7 +48,7 @@ class ModbusFan(BaseSwitch, FanEntity): await self.async_turn(self.command_on) @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if fan is on. This is needed due to the ongoing conversion of fan. diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 7b98a3a9787..845d27488ad 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -161,7 +161,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): return False @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if device is on.""" if not self.data["data"]["switchMode"]: return self._attr_is_on diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index df9b1a46683..cafab8b84a4 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -100,7 +100,7 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): await self.info.node.async_set_value(self._target_value, 0) @property - def is_on(self) -> bool | None: # type: ignore + def is_on(self) -> bool | None: """Return true if device is on (speed above 0).""" if self.info.primary_value.value is None: # guard missing value diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index 390ba6eaf0b..a680a8fb04a 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -65,7 +65,7 @@ class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity): self._target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY) @property - def is_on(self) -> bool | None: # type: ignore + def is_on(self) -> bool | None: """Return a boolean for the state of the switch.""" if self.info.primary_value.value is None: # guard missing value @@ -107,7 +107,7 @@ class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity): self._update_state() @property - def is_on(self) -> bool | None: # type: ignore + def is_on(self) -> bool | None: """Return a boolean for the state of the switch.""" return self._state diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index db9484233bc..b04224651fe 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -927,17 +927,19 @@ class ToggleEntity(Entity): """An abstract class for entities that can be turned on and off.""" entity_description: ToggleEntityDescription - _attr_is_on: bool + _attr_is_on: bool | None = None _attr_state: None = None @property @final - def state(self) -> str | None: + def state(self) -> Literal["on", "off"] | None: """Return the state.""" - return STATE_ON if self.is_on else STATE_OFF + if (is_on := self.is_on) is None: + return None + return STATE_ON if is_on else STATE_OFF @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return True if entity is on.""" return self._attr_is_on diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index a73bba96fba..daa4902f7fe 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -12,7 +12,13 @@ from homeassistant.components.light import ( COLOR_MODE_BRIGHTNESS, COLOR_MODE_RGBW, ) -from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON +from homeassistant.const import ( + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) from homeassistant.helpers import device_registry as dr from .conftest import async_setup_entity, mock_feature @@ -226,7 +232,7 @@ async def test_wlightbox_s_init(wlightbox_s, hass, config): assert color_modes == [COLOR_MODE_BRIGHTNESS] assert ATTR_BRIGHTNESS not in state.attributes - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN device_registry = dr.async_get(hass) device = device_registry.async_get(entry.device_id) @@ -327,7 +333,7 @@ async def test_wlightbox_init(wlightbox, hass, config): assert ATTR_BRIGHTNESS not in state.attributes assert ATTR_RGBW_COLOR not in state.attributes - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN device_registry = dr.async_get(hass) device = device_registry.async_get(entry.device_id) diff --git a/tests/components/blebox/test_switch.py b/tests/components/blebox/test_switch.py index 74aeadc33bb..b494687e539 100644 --- a/tests/components/blebox/test_switch.py +++ b/tests/components/blebox/test_switch.py @@ -12,6 +12,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_OFF, STATE_ON, + STATE_UNKNOWN, ) from homeassistant.helpers import device_registry as dr @@ -202,7 +203,7 @@ async def test_switchbox_d_init(switchbox_d, hass, config): state = hass.states.get(entity_ids[0]) assert state.name == "switchBoxD-0.relay" assert state.attributes[ATTR_DEVICE_CLASS] == SwitchDeviceClass.SWITCH - assert state.state == STATE_OFF # NOTE: should instead be STATE_UNKNOWN? + assert state.state == STATE_UNKNOWN device_registry = dr.async_get(hass) device = device_registry.async_get(entry.device_id) @@ -219,7 +220,7 @@ async def test_switchbox_d_init(switchbox_d, hass, config): state = hass.states.get(entity_ids[1]) assert state.name == "switchBoxD-1.relay" assert state.attributes[ATTR_DEVICE_CLASS] == SwitchDeviceClass.SWITCH - assert state.state == STATE_OFF # NOTE: should instead be STATE_UNKNOWN? + assert state.state == STATE_UNKNOWN device_registry = dr.async_get(hass) device = device_registry.async_get(entry.device_id) diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py index 78151c5fa9c..8b68dc7bf47 100644 --- a/tests/components/rfxtrx/test_light.py +++ b/tests/components/rfxtrx/test_light.py @@ -5,6 +5,7 @@ import pytest from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.rfxtrx import DOMAIN +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import State from tests.common import MockConfigEntry, mock_restore_cache @@ -25,7 +26,7 @@ async def test_one_light(hass, rfxtrx): state = hass.states.get("light.ac_213c7f2_16") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 213c7f2:16" await hass.services.async_call( @@ -132,17 +133,17 @@ async def test_several_lights(hass, rfxtrx): state = hass.states.get("light.ac_213c7f2_48") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 213c7f2:48" state = hass.states.get("light.ac_118cdea_2") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 118cdea:2" state = hass.states.get("light.ac_1118cdea_2") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 1118cdea:2" await rfxtrx.signal("0b1100cd0213c7f230010f71") diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 94adf4a980e..6c22ee02920 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -5,6 +5,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.rfxtrx import DOMAIN +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import State from tests.common import MockConfigEntry, mock_restore_cache @@ -28,7 +29,7 @@ async def test_one_switch(hass, rfxtrx): state = hass.states.get("switch.ac_213c7f2_16") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 213c7f2:16" await hass.services.async_call( @@ -90,17 +91,17 @@ async def test_several_switches(hass, rfxtrx): state = hass.states.get("switch.ac_213c7f2_48") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 213c7f2:48" state = hass.states.get("switch.ac_118cdea_2") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 118cdea:2" state = hass.states.get("switch.ac_1118cdea_2") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 1118cdea:2" @@ -142,22 +143,22 @@ async def test_switch_events(hass, rfxtrx): state = hass.states.get("switch.ac_213c7f2_16") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 213c7f2:16" state = hass.states.get("switch.ac_213c7f2_5") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 213c7f2:5" # "16: On" await rfxtrx.signal("0b1100100213c7f210010f70") - assert hass.states.get("switch.ac_213c7f2_5").state == "off" + assert hass.states.get("switch.ac_213c7f2_5").state == STATE_UNKNOWN assert hass.states.get("switch.ac_213c7f2_16").state == "on" # "16: Off" await rfxtrx.signal("0b1100100213c7f210000f70") - assert hass.states.get("switch.ac_213c7f2_5").state == "off" + assert hass.states.get("switch.ac_213c7f2_5").state == STATE_UNKNOWN assert hass.states.get("switch.ac_213c7f2_16").state == "off" # "5: On"