Ensure lights added after group is created have the correct state (#41034)
This commit is contained in:
parent
ed171a885b
commit
78dfaa72a2
2 changed files with 283 additions and 23 deletions
|
@ -91,6 +91,7 @@ class GroupIntegrationRegistry:
|
||||||
"""Class to hold a registry of integrations."""
|
"""Class to hold a registry of integrations."""
|
||||||
|
|
||||||
on_off_mapping: Dict[str, str] = {STATE_ON: STATE_OFF}
|
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] = {}
|
on_states_by_domain: Dict[str, Set] = {}
|
||||||
exclude_domains: Set = set()
|
exclude_domains: Set = set()
|
||||||
|
|
||||||
|
@ -99,11 +100,14 @@ class GroupIntegrationRegistry:
|
||||||
self.exclude_domains.add(current_domain.get())
|
self.exclude_domains.add(current_domain.get())
|
||||||
|
|
||||||
def on_off_states(self, on_states: Set, off_state: str) -> None:
|
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:
|
for on_state in on_states:
|
||||||
if on_state not in self.on_off_mapping:
|
if on_state not in self.on_off_mapping:
|
||||||
self.on_off_mapping[on_state] = off_state
|
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)
|
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}
|
data = {ATTR_ENTITY_ID: self.tracking, ATTR_ORDER: self._order}
|
||||||
if not self.user_defined:
|
if not self.user_defined:
|
||||||
data[ATTR_AUTO] = True
|
data[ATTR_AUTO] = True
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -577,6 +582,7 @@ class Group(Entity):
|
||||||
return
|
return
|
||||||
|
|
||||||
excluded_domains = self.hass.data[REG_KEY].exclude_domains
|
excluded_domains = self.hass.data[REG_KEY].exclude_domains
|
||||||
|
|
||||||
tracking = []
|
tracking = []
|
||||||
trackable = []
|
trackable = []
|
||||||
for ent_id in entity_ids:
|
for ent_id in entity_ids:
|
||||||
|
@ -592,6 +598,7 @@ class Group(Entity):
|
||||||
@callback
|
@callback
|
||||||
def _async_start(self, *_):
|
def _async_start(self, *_):
|
||||||
"""Start tracking members and write state."""
|
"""Start tracking members and write state."""
|
||||||
|
self._reset_tracked_state()
|
||||||
self._async_start_tracking()
|
self._async_start_tracking()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@ -625,15 +632,14 @@ class Group(Entity):
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Handle addition to Home Assistant."""
|
"""Handle addition to Home Assistant."""
|
||||||
if self.tracking:
|
|
||||||
self._reset_tracked_state()
|
|
||||||
|
|
||||||
if self.hass.state != CoreState.running:
|
if self.hass.state != CoreState.running:
|
||||||
self.hass.bus.async_listen_once(
|
self.hass.bus.async_listen_once(
|
||||||
EVENT_HOMEASSISTANT_START, self._async_start
|
EVENT_HOMEASSISTANT_START, self._async_start
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.tracking:
|
||||||
|
self._reset_tracked_state()
|
||||||
self._async_start_tracking()
|
self._async_start_tracking()
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self):
|
async def async_will_remove_from_hass(self):
|
||||||
|
@ -671,19 +677,26 @@ class Group(Entity):
|
||||||
if state is not None:
|
if state is not None:
|
||||||
self._see_state(state)
|
self._see_state(state)
|
||||||
|
|
||||||
def _see_state(self, state):
|
def _see_state(self, new_state):
|
||||||
"""Keep track of the the state."""
|
"""Keep track of the the state."""
|
||||||
entity_id = state.entity_id
|
entity_id = new_state.entity_id
|
||||||
domain = state.domain
|
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(
|
if domain not in registry.on_states_by_domain:
|
||||||
domain, {STATE_ON}
|
# Handle the group of a group case
|
||||||
)
|
if state in registry.on_off_mapping:
|
||||||
self._on_off[entity_id] = state.state in domain_on_state
|
self._on_states.add(state)
|
||||||
self._assumed[entity_id] = state.attributes.get(ATTR_ASSUMED_STATE)
|
elif state in registry.off_on_mapping:
|
||||||
|
self._on_states.add(registry.off_on_mapping[state])
|
||||||
if domain in self.hass.data[REG_KEY].on_states_by_domain:
|
self._on_off[entity_id] = state in registry.on_off_mapping
|
||||||
self._on_states.update(domain_on_state)
|
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
|
@callback
|
||||||
def _async_update_group_state(self, tr_state=None):
|
def _async_update_group_state(self, tr_state=None):
|
||||||
|
@ -726,7 +739,6 @@ class Group(Entity):
|
||||||
# on state, we use STATE_ON/STATE_OFF
|
# on state, we use STATE_ON/STATE_OFF
|
||||||
else:
|
else:
|
||||||
on_state = STATE_ON
|
on_state = STATE_ON
|
||||||
|
|
||||||
group_is_on = self.mode(self._on_off.values())
|
group_is_on = self.mode(self._on_off.values())
|
||||||
if group_is_on:
|
if group_is_on:
|
||||||
self._state = on_state
|
self._state = on_state
|
||||||
|
|
|
@ -763,7 +763,6 @@ async def test_group_climate_all_cool(hass):
|
||||||
hass.states.async_set("climate.two", "cool")
|
hass.states.async_set("climate.two", "cool")
|
||||||
hass.states.async_set("climate.three", "cool")
|
hass.states.async_set("climate.three", "cool")
|
||||||
|
|
||||||
assert await async_setup_component(hass, "climate", {})
|
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
"group",
|
"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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass.states.get("group.group_zero").state == STATE_ON
|
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.one", "armed_away")
|
||||||
hass.states.async_set("alarm_control_panel.two", "armed_home")
|
hass.states.async_set("alarm_control_panel.two", "armed_home")
|
||||||
hass.states.async_set("alarm_control_panel.three", "armed_away")
|
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(
|
assert await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
"group",
|
"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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass.states.get("group.group_zero").state == STATE_ON
|
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.one", "docked")
|
||||||
hass.states.async_set("vacuum.two", "off")
|
hass.states.async_set("vacuum.two", "off")
|
||||||
hass.states.async_set("vacuum.three", "off")
|
hass.states.async_set("vacuum.three", "off")
|
||||||
|
hass.state = CoreState.stopped
|
||||||
|
|
||||||
assert await async_setup_component(hass, "vacuum", {})
|
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
"group",
|
"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()
|
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
|
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.two", "not_home")
|
||||||
hass.states.async_set("device_tracker.three", "not_home")
|
hass.states.async_set("device_tracker.three", "not_home")
|
||||||
|
|
||||||
assert await async_setup_component(hass, "device_tracker", {})
|
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
"group",
|
"group",
|
||||||
|
@ -916,7 +920,6 @@ async def test_light_removed(hass):
|
||||||
hass.states.async_set("light.two", "off")
|
hass.states.async_set("light.two", "off")
|
||||||
hass.states.async_set("light.three", "on")
|
hass.states.async_set("light.three", "on")
|
||||||
|
|
||||||
assert await async_setup_component(hass, "light", {})
|
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
"group",
|
"group",
|
||||||
|
@ -943,7 +946,6 @@ async def test_switch_removed(hass):
|
||||||
hass.states.async_set("switch.three", "on")
|
hass.states.async_set("switch.three", "on")
|
||||||
|
|
||||||
hass.state = CoreState.stopped
|
hass.state = CoreState.stopped
|
||||||
assert await async_setup_component(hass, "switch", {})
|
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
"group",
|
"group",
|
||||||
|
@ -956,6 +958,8 @@ async def test_switch_removed(hass):
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass.states.get("group.group_zero").state == "unknown"
|
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)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -965,3 +969,247 @@ async def test_switch_removed(hass):
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass.states.get("group.group_zero").state == "off"
|
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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue