Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow binary sensor state to be None #60193

Merged
merged 14 commits into from
Dec 22, 2021
10 changes: 6 additions & 4 deletions homeassistant/components/binary_sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import final
from typing import Literal, final

import voluptuous as vol

Expand All @@ -18,7 +18,7 @@
)
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType, StateType
from homeassistant.helpers.typing import ConfigType

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -200,6 +200,8 @@ def is_on(self) -> bool | None:

@final
@property
def state(self) -> StateType:
def state(self) -> Literal["on", "off"] | None:
"""Return the state of the binary sensor."""
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
2 changes: 1 addition & 1 deletion tests/components/binary_sensor/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
def test_state():
"""Test binary sensor state."""
sensor = binary_sensor.BinarySensorEntity()
assert sensor.state == STATE_OFF
assert sensor.state is None
with mock.patch(
"homeassistant.components.binary_sensor.BinarySensorEntity.is_on",
new=False,
Expand Down
12 changes: 9 additions & 3 deletions tests/components/group/test_binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
"""The tests for the Group Binary Sensor platform."""
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.group import DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.const import (
ATTR_ENTITY_ID,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component

Expand Down Expand Up @@ -65,7 +71,7 @@ async def test_state_reporting_all(hass):
hass.states.async_set("binary_sensor.test1", STATE_ON)
hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE)
await hass.async_block_till_done()
assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF
assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN

hass.states.async_set("binary_sensor.test1", STATE_ON)
hass.states.async_set("binary_sensor.test2", STATE_OFF)
Expand Down Expand Up @@ -114,7 +120,7 @@ async def test_state_reporting_any(hass):
hass.states.async_set("binary_sensor.test1", STATE_ON)
hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE)
await hass.async_block_till_done()
assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF
assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN

hass.states.async_set("binary_sensor.test1", STATE_ON)
hass.states.async_set("binary_sensor.test2", STATE_OFF)
Expand Down
12 changes: 6 additions & 6 deletions tests/components/homematicip_cloud/test_binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
ATTR_RSSI_DEVICE,
ATTR_SABOTAGE,
)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
from homeassistant.setup import async_setup_component

from .helper import async_manipulate_test_data, get_and_check_entity_basics
Expand Down Expand Up @@ -152,7 +152,7 @@ async def test_hmip_contact_interface(hass, default_mock_hap_factory):

await async_manipulate_test_data(hass, hmip_device, "windowState", None)
ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_OFF
assert ha_state.state == STATE_UNKNOWN


async def test_hmip_shutter_contact(hass, default_mock_hap_factory):
Expand Down Expand Up @@ -185,7 +185,7 @@ async def test_hmip_shutter_contact(hass, default_mock_hap_factory):

await async_manipulate_test_data(hass, hmip_device, "windowState", None)
ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_OFF
assert ha_state.state == STATE_UNKNOWN

# test common attributes
assert ha_state.attributes[ATTR_RSSI_DEVICE] == -54
Expand Down Expand Up @@ -215,7 +215,7 @@ async def test_hmip_shutter_contact_optical(hass, default_mock_hap_factory):

await async_manipulate_test_data(hass, hmip_device, "windowState", None)
ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_OFF
assert ha_state.state == STATE_UNKNOWN

# test common attributes
assert ha_state.attributes[ATTR_RSSI_DEVICE] == -72
Expand Down Expand Up @@ -562,7 +562,7 @@ async def test_hmip_multi_contact_interface(hass, default_mock_hap_factory):

await async_manipulate_test_data(hass, hmip_device, "windowState", None, channel=5)
ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_OFF
assert ha_state.state == STATE_UNKNOWN

ha_state, hmip_device = get_and_check_entity_basics(
hass,
Expand All @@ -572,4 +572,4 @@ async def test_hmip_multi_contact_interface(hass, default_mock_hap_factory):
"HmIP-FCI6",
)

assert ha_state.state == STATE_OFF
assert ha_state.state == STATE_UNKNOWN
8 changes: 4 additions & 4 deletions tests/components/mobile_app/test_binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Entity tests for mobile_app."""
from http import HTTPStatus

from homeassistant.const import STATE_OFF
from homeassistant.const import STATE_UNKNOWN
from homeassistant.helpers import device_registry as dr


Expand Down Expand Up @@ -198,7 +198,7 @@ async def test_register_sensor_no_state(hass, create_registrations, webhook_clie

assert entity.domain == "binary_sensor"
assert entity.name == "Test 1 Is Charging"
assert entity.state == STATE_OFF # Binary sensor defaults to off
assert entity.state == STATE_UNKNOWN

reg_resp = await webhook_client.post(
webhook_url,
Expand All @@ -223,7 +223,7 @@ async def test_register_sensor_no_state(hass, create_registrations, webhook_clie

assert entity.domain == "binary_sensor"
assert entity.name == "Test 1 Backup Is Charging"
assert entity.state == STATE_OFF # Binary sensor defaults to off
assert entity.state == STATE_UNKNOWN


async def test_update_sensor_no_state(hass, create_registrations, webhook_client):
Expand Down Expand Up @@ -270,4 +270,4 @@ async def test_update_sensor_no_state(hass, create_registrations, webhook_client
assert json == {"is_charging": {"success": True}}

updated_entity = hass.states.get("binary_sensor.test_1_is_charging")
assert updated_entity.state == STATE_OFF # Binary sensor defaults to off
assert updated_entity.state == STATE_UNKNOWN
3 changes: 2 additions & 1 deletion tests/components/modbus/test_binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import State

Expand Down Expand Up @@ -141,7 +142,7 @@ async def test_all_binary_sensor(hass, expected, mock_do_cycle):
(
[0x00],
True,
STATE_OFF,
STATE_UNKNOWN,
STATE_UNAVAILABLE,
),
],
Expand Down
17 changes: 9 additions & 8 deletions tests/components/mqtt/test_binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
import homeassistant.core as ha
from homeassistant.setup import async_setup_component
Expand Down Expand Up @@ -256,7 +257,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock):

state = hass.states.get("binary_sensor.test")

assert state.state == STATE_OFF
assert state.state == STATE_UNKNOWN

async_fire_mqtt_message(hass, "test-topic", "ON")
state = hass.states.get("binary_sensor.test")
Expand Down Expand Up @@ -286,11 +287,11 @@ async def test_invalid_sensor_value_via_mqtt_message(hass, mqtt_mock, caplog):

state = hass.states.get("binary_sensor.test")

assert state.state == STATE_OFF
assert state.state == STATE_UNKNOWN

async_fire_mqtt_message(hass, "test-topic", "0N")
state = hass.states.get("binary_sensor.test")
assert state.state == STATE_OFF
assert state.state == STATE_UNKNOWN
assert "No matching payload found for entity" in caplog.text
caplog.clear()
assert "No matching payload found for entity" not in caplog.text
Expand Down Expand Up @@ -325,7 +326,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template(hass, mqtt_moc
await hass.async_block_till_done()

state = hass.states.get("binary_sensor.test")
assert state.state == STATE_OFF
assert state.state == STATE_UNKNOWN

async_fire_mqtt_message(hass, "test-topic", "")
state = hass.states.get("binary_sensor.test")
Expand Down Expand Up @@ -357,7 +358,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template2(
await hass.async_block_till_done()

state = hass.states.get("binary_sensor.test")
assert state.state == STATE_OFF
assert state.state == STATE_UNKNOWN

async_fire_mqtt_message(hass, "test-topic", "on")
state = hass.states.get("binary_sensor.test")
Expand Down Expand Up @@ -395,7 +396,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_
await hass.async_block_till_done()

state = hass.states.get("binary_sensor.test")
assert state.state == STATE_OFF
assert state.state == STATE_UNKNOWN

async_fire_mqtt_message(hass, "test-topic", b"\x01")
state = hass.states.get("binary_sensor.test")
Expand Down Expand Up @@ -427,11 +428,11 @@ async def test_setting_sensor_value_via_mqtt_message_empty_template(
await hass.async_block_till_done()

state = hass.states.get("binary_sensor.test")
assert state.state == STATE_OFF
assert state.state == STATE_UNKNOWN

async_fire_mqtt_message(hass, "test-topic", "DEF")
state = hass.states.get("binary_sensor.test")
assert state.state == STATE_OFF
assert state.state == STATE_UNKNOWN
assert "Empty template output" in caplog.text

async_fire_mqtt_message(hass, "test-topic", "ABC")
Expand Down
7 changes: 4 additions & 3 deletions tests/components/rflink/test_binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
import homeassistant.core as ha
import homeassistant.util.dt as dt_util
Expand Down Expand Up @@ -53,7 +54,7 @@ async def test_default_setup(hass, monkeypatch):
# test default state of sensor loaded from config
config_sensor = hass.states.get("binary_sensor.test")
assert config_sensor
assert config_sensor.state == STATE_OFF
assert config_sensor.state == STATE_UNKNOWN
assert config_sensor.attributes["device_class"] == "door"

# test on event for config sensor
Expand Down Expand Up @@ -95,7 +96,7 @@ async def test_entity_availability(hass, monkeypatch):
)

# Entities are available by default
assert hass.states.get("binary_sensor.test").state == STATE_OFF
assert hass.states.get("binary_sensor.test").state == STATE_UNKNOWN

# Mock a disconnect of the Rflink device
disconnect_callback()
Expand All @@ -113,7 +114,7 @@ async def test_entity_availability(hass, monkeypatch):
await hass.async_block_till_done()

# Entities should be available again
assert hass.states.get("binary_sensor.test").state == STATE_OFF
assert hass.states.get("binary_sensor.test").state == STATE_UNKNOWN


async def test_off_delay(hass, legacy_patchable_time, monkeypatch):
Expand Down
19 changes: 10 additions & 9 deletions tests/components/rfxtrx/test_binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.components.rfxtrx.const import ATTR_EVENT
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import State

from tests.common import MockConfigEntry, mock_restore_cache
Expand Down Expand Up @@ -32,7 +33,7 @@ async def test_one(hass, rfxtrx):

state = hass.states.get("binary_sensor.ac_213c7f2_48")
assert state
assert state.state == "off"
assert state.state == STATE_UNKNOWN
assert state.attributes.get("friendly_name") == "AC 213c7f2:48"


Expand All @@ -57,7 +58,7 @@ async def test_one_pt2262(hass, rfxtrx):

state = hass.states.get("binary_sensor.pt2262_22670e")
assert state
assert state.state == "off" # probably aught to be unknown
assert state.state == STATE_UNKNOWN
assert state.attributes.get("friendly_name") == "PT2262 22670e"

await rfxtrx.signal("0913000022670e013970")
Expand All @@ -84,12 +85,12 @@ async def test_pt2262_unconfigured(hass, rfxtrx):

state = hass.states.get("binary_sensor.pt2262_22670e")
assert state
assert state.state == "off" # probably aught to be unknown
assert state.state == STATE_UNKNOWN
assert state.attributes.get("friendly_name") == "PT2262 22670e"

state = hass.states.get("binary_sensor.pt2262_226707")
assert state
assert state.state == "off" # probably aught to be unknown
assert state.state == STATE_UNKNOWN
assert state.attributes.get("friendly_name") == "PT2262 226707"


Expand Down Expand Up @@ -133,17 +134,17 @@ async def test_several(hass, rfxtrx):

state = hass.states.get("binary_sensor.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("binary_sensor.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("binary_sensor.ac_118cdea_3")
assert state
assert state.state == "off"
assert state.state == STATE_UNKNOWN
assert state.attributes.get("friendly_name") == "AC 118cdea:3"

# "2: Group on"
Expand Down Expand Up @@ -214,7 +215,7 @@ async def test_off_delay(hass, rfxtrx, timestep):

state = hass.states.get("binary_sensor.ac_118cdea_2")
assert state
assert state.state == "off"
assert state.state == STATE_UNKNOWN

await rfxtrx.signal("0b1100100118cdea02010f70")
state = hass.states.get("binary_sensor.ac_118cdea_2")
Expand Down Expand Up @@ -317,5 +318,5 @@ async def test_pt2262_duplicate_id(hass, rfxtrx):

state = hass.states.get("binary_sensor.pt2262_22670e")
assert state
assert state.state == "off" # probably aught to be unknown
assert state.state == STATE_UNKNOWN
assert state.attributes.get("friendly_name") == "PT2262 22670e"
7 changes: 4 additions & 3 deletions tests/components/rfxtrx/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from homeassistant import config_entries, data_entry_flow
from homeassistant.components.rfxtrx import DOMAIN, config_flow
from homeassistant.const import STATE_UNKNOWN
from homeassistant.helpers import device_registry as dr, entity_registry as er

from tests.common import MockConfigEntry
Expand Down Expand Up @@ -367,7 +368,7 @@ async def test_options_add_device(hass):

state = hass.states.get("binary_sensor.ac_213c7f2_48")
assert state
assert state.state == "off"
assert state.state == STATE_UNKNOWN
assert state.attributes.get("friendly_name") == "AC 213c7f2:48"


Expand Down Expand Up @@ -456,7 +457,7 @@ async def test_options_add_remove_device(hass):

state = hass.states.get("binary_sensor.ac_213c7f2_48")
assert state
assert state.state == "off"
assert state.state == STATE_UNKNOWN
assert state.attributes.get("friendly_name") == "AC 213c7f2:48"

device_registry = dr.async_get(hass)
Expand Down Expand Up @@ -900,7 +901,7 @@ async def test_options_add_and_configure_device(hass):

state = hass.states.get("binary_sensor.pt2262_22670e")
assert state
assert state.state == "off"
assert state.state == STATE_UNKNOWN
assert state.attributes.get("friendly_name") == "PT2262 22670e"

device_registry = dr.async_get(hass)
Expand Down
Loading