Ensure lights added after group is created have the correct state (#41034)

This commit is contained in:
J. Nick Koston 2020-10-03 14:29:41 -05:00 committed by GitHub
parent ed171a885b
commit 78dfaa72a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 283 additions and 23 deletions

View file

@ -91,6 +91,7 @@ class GroupIntegrationRegistry:
"""Class to hold a registry of integrations."""
on_off_mapping: Dict[str, str] = {STATE_ON: STATE_OFF}
off_on_mapping: Dict[str, str] = {STATE_OFF: STATE_ON}
on_states_by_domain: Dict[str, Set] = {}
exclude_domains: Set = set()
@ -99,11 +100,14 @@ class GroupIntegrationRegistry:
self.exclude_domains.add(current_domain.get())
def on_off_states(self, on_states: Set, off_state: str) -> None:
"""Registry on and off states for the current domain."""
"""Register on and off states for the current domain."""
for on_state in on_states:
if on_state not in self.on_off_mapping:
self.on_off_mapping[on_state] = off_state
if len(on_states) == 1 and off_state not in self.off_on_mapping:
self.off_on_mapping[off_state] = list(on_states)[0]
self.on_states_by_domain[current_domain.get()] = set(on_states)
@ -543,6 +547,7 @@ class Group(Entity):
data = {ATTR_ENTITY_ID: self.tracking, ATTR_ORDER: self._order}
if not self.user_defined:
data[ATTR_AUTO] = True
return data
@property
@ -577,6 +582,7 @@ class Group(Entity):
return
excluded_domains = self.hass.data[REG_KEY].exclude_domains
tracking = []
trackable = []
for ent_id in entity_ids:
@ -592,6 +598,7 @@ class Group(Entity):
@callback
def _async_start(self, *_):
"""Start tracking members and write state."""
self._reset_tracked_state()
self._async_start_tracking()
self.async_write_ha_state()
@ -625,15 +632,14 @@ class Group(Entity):
async def async_added_to_hass(self):
"""Handle addition to Home Assistant."""
if self.tracking:
self._reset_tracked_state()
if self.hass.state != CoreState.running:
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, self._async_start
)
return
if self.tracking:
self._reset_tracked_state()
self._async_start_tracking()
async def async_will_remove_from_hass(self):
@ -671,19 +677,26 @@ class Group(Entity):
if state is not None:
self._see_state(state)
def _see_state(self, state):
def _see_state(self, new_state):
"""Keep track of the the state."""
entity_id = state.entity_id
domain = state.domain
entity_id = new_state.entity_id
domain = new_state.domain
state = new_state.state
registry = self.hass.data[REG_KEY]
self._assumed[entity_id] = new_state.attributes.get(ATTR_ASSUMED_STATE)
domain_on_state = self.hass.data[REG_KEY].on_states_by_domain.get(
domain, {STATE_ON}
)
self._on_off[entity_id] = state.state in domain_on_state
self._assumed[entity_id] = state.attributes.get(ATTR_ASSUMED_STATE)
if domain in self.hass.data[REG_KEY].on_states_by_domain:
self._on_states.update(domain_on_state)
if domain not in registry.on_states_by_domain:
# Handle the group of a group case
if state in registry.on_off_mapping:
self._on_states.add(state)
elif state in registry.off_on_mapping:
self._on_states.add(registry.off_on_mapping[state])
self._on_off[entity_id] = state in registry.on_off_mapping
else:
entity_on_state = registry.on_states_by_domain[domain]
if domain in self.hass.data[REG_KEY].on_states_by_domain:
self._on_states.update(entity_on_state)
self._on_off[entity_id] = state in entity_on_state
@callback
def _async_update_group_state(self, tr_state=None):
@ -726,7 +739,6 @@ class Group(Entity):
# on state, we use STATE_ON/STATE_OFF
else:
on_state = STATE_ON
group_is_on = self.mode(self._on_off.values())
if group_is_on:
self._state = on_state

View file

@ -763,7 +763,6 @@ async def test_group_climate_all_cool(hass):
hass.states.async_set("climate.two", "cool")
hass.states.async_set("climate.three", "cool")
assert await async_setup_component(hass, "climate", {})
assert await async_setup_component(
hass,
"group",
@ -773,6 +772,7 @@ async def test_group_climate_all_cool(hass):
}
},
)
assert await async_setup_component(hass, "climate", {})
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == STATE_ON
@ -804,8 +804,8 @@ async def test_group_alarm(hass):
hass.states.async_set("alarm_control_panel.one", "armed_away")
hass.states.async_set("alarm_control_panel.two", "armed_home")
hass.states.async_set("alarm_control_panel.three", "armed_away")
hass.state = CoreState.stopped
assert await async_setup_component(hass, "alarm_control_panel", {})
assert await async_setup_component(
hass,
"group",
@ -817,8 +817,10 @@ async def test_group_alarm(hass):
}
},
)
assert await async_setup_component(hass, "alarm_control_panel", {})
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == STATE_ON
@ -850,8 +852,8 @@ async def test_group_vacuum_off(hass):
hass.states.async_set("vacuum.one", "docked")
hass.states.async_set("vacuum.two", "off")
hass.states.async_set("vacuum.three", "off")
hass.state = CoreState.stopped
assert await async_setup_component(hass, "vacuum", {})
assert await async_setup_component(
hass,
"group",
@ -861,8 +863,11 @@ async def test_group_vacuum_off(hass):
}
},
)
assert await async_setup_component(hass, "vacuum", {})
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == STATE_OFF
@ -893,7 +898,6 @@ async def test_device_tracker_not_home(hass):
hass.states.async_set("device_tracker.two", "not_home")
hass.states.async_set("device_tracker.three", "not_home")
assert await async_setup_component(hass, "device_tracker", {})
assert await async_setup_component(
hass,
"group",
@ -916,7 +920,6 @@ async def test_light_removed(hass):
hass.states.async_set("light.two", "off")
hass.states.async_set("light.three", "on")
assert await async_setup_component(hass, "light", {})
assert await async_setup_component(
hass,
"group",
@ -943,7 +946,6 @@ async def test_switch_removed(hass):
hass.states.async_set("switch.three", "on")
hass.state = CoreState.stopped
assert await async_setup_component(hass, "switch", {})
assert await async_setup_component(
hass,
"group",
@ -956,6 +958,8 @@ async def test_switch_removed(hass):
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == "unknown"
assert await async_setup_component(hass, "switch", {})
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
@ -965,3 +969,247 @@ async def test_switch_removed(hass):
await hass.async_block_till_done()
assert hass.states.get("group.group_zero").state == "off"
async def test_lights_added_after_group(hass):
"""Test lights added after group."""
entity_ids = [
"light.living_front_ri",
"light.living_back_lef",
"light.living_back_cen",
"light.living_front_le",
"light.living_front_ce",
"light.living_back_rig",
]
assert await async_setup_component(
hass,
"group",
{
"group": {
"living_room_downlights": {"entities": entity_ids},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.living_room_downlights").state == "unknown"
for entity_id in entity_ids:
hass.states.async_set(entity_id, "off")
await hass.async_block_till_done()
assert hass.states.get("group.living_room_downlights").state == "off"
async def test_lights_added_before_group(hass):
"""Test lights added before group."""
entity_ids = [
"light.living_front_ri",
"light.living_back_lef",
"light.living_back_cen",
"light.living_front_le",
"light.living_front_ce",
"light.living_back_rig",
]
for entity_id in entity_ids:
hass.states.async_set(entity_id, "off")
await hass.async_block_till_done()
assert await async_setup_component(
hass,
"group",
{
"group": {
"living_room_downlights": {"entities": entity_ids},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("group.living_room_downlights").state == "off"
async def test_cover_added_after_group(hass):
"""Test cover added after group."""
entity_ids = [
"cover.upstairs",
"cover.downstairs",
]
assert await async_setup_component(
hass,
"group",
{
"group": {
"shades": {"entities": entity_ids},
}
},
)
await hass.async_block_till_done()
for entity_id in entity_ids:
hass.states.async_set(entity_id, "open")
await hass.async_block_till_done()
await hass.async_block_till_done()
assert hass.states.get("group.shades").state == "open"
for entity_id in entity_ids:
hass.states.async_set(entity_id, "closed")
await hass.async_block_till_done()
assert hass.states.get("group.shades").state == "closed"
async def test_group_that_references_a_group_of_lights(hass):
"""Group that references a group of lights."""
entity_ids = [
"light.living_front_ri",
"light.living_back_lef",
]
hass.state = CoreState.stopped
for entity_id in entity_ids:
hass.states.async_set(entity_id, "off")
await hass.async_block_till_done()
assert await async_setup_component(
hass,
"group",
{
"group": {
"living_room_downlights": {"entities": entity_ids},
"grouped_group": {
"entities": ["group.living_room_downlights", *entity_ids]
},
}
},
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert hass.states.get("group.living_room_downlights").state == "off"
assert hass.states.get("group.grouped_group").state == "off"
async def test_group_that_references_a_group_of_covers(hass):
"""Group that references a group of covers."""
entity_ids = [
"cover.living_front_ri",
"cover.living_back_lef",
]
hass.state = CoreState.stopped
for entity_id in entity_ids:
hass.states.async_set(entity_id, "closed")
await hass.async_block_till_done()
assert await async_setup_component(
hass,
"group",
{
"group": {
"living_room_downcover": {"entities": entity_ids},
"grouped_group": {
"entities": ["group.living_room_downlights", *entity_ids]
},
}
},
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert hass.states.get("group.living_room_downcover").state == "closed"
assert hass.states.get("group.grouped_group").state == "closed"
async def test_group_that_references_two_groups_of_covers(hass):
"""Group that references a group of covers."""
entity_ids = [
"cover.living_front_ri",
"cover.living_back_lef",
]
hass.state = CoreState.stopped
for entity_id in entity_ids:
hass.states.async_set(entity_id, "closed")
await hass.async_block_till_done()
assert await async_setup_component(
hass,
"group",
{
"group": {
"living_room_downcover": {"entities": entity_ids},
"living_room_upcover": {"entities": entity_ids},
"grouped_group": {
"entities": [
"group.living_room_downlights",
"group.living_room_upcover",
]
},
}
},
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert hass.states.get("group.living_room_downcover").state == "closed"
assert hass.states.get("group.living_room_upcover").state == "closed"
assert hass.states.get("group.grouped_group").state == "closed"
async def test_group_that_references_two_types_of_groups(hass):
"""Group that references a group of covers and device_trackers."""
group_1_entity_ids = [
"cover.living_front_ri",
"cover.living_back_lef",
]
group_2_entity_ids = [
"device_tracker.living_front_ri",
"device_tracker.living_back_lef",
]
hass.state = CoreState.stopped
for entity_id in group_1_entity_ids:
hass.states.async_set(entity_id, "closed")
for entity_id in group_2_entity_ids:
hass.states.async_set(entity_id, "home")
await hass.async_block_till_done()
assert await async_setup_component(
hass,
"group",
{
"group": {
"covers": {"entities": group_1_entity_ids},
"device_trackers": {"entities": group_2_entity_ids},
"grouped_group": {
"entities": ["group.covers", "group.device_trackers"]
},
}
},
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert hass.states.get("group.covers").state == "closed"
assert hass.states.get("group.device_trackers").state == "home"
assert hass.states.get("group.grouped_group").state == "on"