Cleaned up device_tracker and added tests

This commit is contained in:
Paulus Schoutsen 2014-12-02 21:53:00 -08:00
parent 12c734fa48
commit eef4817804
5 changed files with 429 additions and 171 deletions

View File

@ -0,0 +1,41 @@
"""
custom_components.device_tracker.test
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides a mock device scanner.
"""
def get_scanner(hass, config):
""" Returns a mock scanner. """
return SCANNER
class MockScanner(object):
""" Mock device scanner. """
def __init__(self):
""" Initialize the MockScanner. """
self.devices_home = []
def come_home(self, device):
""" Make a device come home. """
self.devices_home.append(device)
def leave_home(self, device):
""" Make a device leave the house. """
self.devices_home.remove(device)
def scan_devices(self):
""" Returns a list of fake devices. """
return list(self.devices_home)
def get_device_name(self, device):
"""
Returns a name for a mock device.
Returns None for dev1 for testing.
"""
return None if device == 'dev1' else device.upper()
SCANNER = MockScanner()

View File

@ -0,0 +1,190 @@
"""
ha_test.test_component_group
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests the group compoments.
"""
# pylint: disable=protected-access,too-many-public-methods
import unittest
from datetime import datetime, timedelta
import logging
import os
import homeassistant as ha
import homeassistant.loader as loader
from homeassistant.components import (
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE)
import homeassistant.components.device_tracker as device_tracker
from helper import get_test_home_assistant
def setUpModule(): # pylint: disable=invalid-name
""" Setup to ignore group errors. """
logging.disable(logging.CRITICAL)
class TestComponentsDeviceTracker(unittest.TestCase):
""" Tests homeassistant.components.device_tracker module. """
def setUp(self): # pylint: disable=invalid-name
""" Init needed objects. """
self.hass = get_test_home_assistant()
loader.prepare(self.hass)
self.known_dev_path = self.hass.get_config_path(
device_tracker.KNOWN_DEVICES_FILE)
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
if os.path.isfile(self.known_dev_path):
os.remove(self.known_dev_path)
def test_is_on(self):
""" Test is_on method. """
entity_id = device_tracker.ENTITY_ID_FORMAT.format('test')
self.hass.states.set(entity_id, STATE_HOME)
self.assertTrue(device_tracker.is_on(self.hass, entity_id))
self.hass.states.set(entity_id, STATE_NOT_HOME)
self.assertFalse(device_tracker.is_on(self.hass, entity_id))
def test_setup(self):
""" Test setup method. """
# Bogus config
self.assertFalse(device_tracker.setup(self.hass, {}))
self.assertFalse(
device_tracker.setup(self.hass, {device_tracker.DOMAIN: {}}))
# Test with non-existing component
self.assertFalse(device_tracker.setup(
self.hass, {device_tracker.DOMAIN: {ha.CONF_TYPE: 'nonexisting'}}
))
# Test with a bad known device file around
with open(self.known_dev_path, 'w') as fil:
fil.write("bad data\nbad data\n")
self.assertFalse(device_tracker.setup(self.hass, {
device_tracker.DOMAIN: {ha.CONF_TYPE: 'test'}
}))
def test_device_tracker(self):
""" Test the device tracker class. """
scanner = loader.get_component(
'device_tracker.test').get_scanner(None, None)
scanner.come_home('dev1')
scanner.come_home('dev2')
self.assertTrue(device_tracker.setup(self.hass, {
device_tracker.DOMAIN: {ha.CONF_TYPE: 'test'}
}))
# Ensure a new known devices file has been created.
# Since the device_tracker uses a set internally we cannot
# know what the order of the devices in the known devices file is.
# To ensure all the three expected lines are there, we sort the file
with open(self.known_dev_path) as fil:
self.assertEqual(
['dev1,unknown_device,0,\n', 'dev2,DEV2,0,\n',
'device,name,track,picture\n'],
sorted(fil))
# Write one where we track dev1, dev2
with open(self.known_dev_path, 'w') as fil:
fil.write('device,name,track,picture\n')
fil.write('dev1,Device 1,1,http://example.com/dev1.jpg\n')
fil.write('dev2,Device 2,1,http://example.com/dev2.jpg\n')
scanner.leave_home('dev1')
scanner.come_home('dev3')
self.hass.services.call(
device_tracker.DOMAIN,
device_tracker.SERVICE_DEVICE_TRACKER_RELOAD)
self.hass._pool.block_till_done()
dev1 = device_tracker.ENTITY_ID_FORMAT.format('Device_1')
dev2 = device_tracker.ENTITY_ID_FORMAT.format('Device_2')
dev3 = device_tracker.ENTITY_ID_FORMAT.format('DEV3')
now = datetime.now()
nowNext = now + timedelta(seconds=ha.TIMER_INTERVAL)
nowAlmostMinGone = (now + device_tracker.TIME_DEVICE_NOT_FOUND -
timedelta(seconds=1))
nowMinGone = nowAlmostMinGone + timedelta(seconds=2)
# Test initial is correct
self.assertTrue(device_tracker.is_on(self.hass))
self.assertFalse(device_tracker.is_on(self.hass, dev1))
self.assertTrue(device_tracker.is_on(self.hass, dev2))
self.assertIsNone(self.hass.states.get(dev3))
self.assertEqual(
'http://example.com/dev1.jpg',
self.hass.states.get(dev1).attributes.get(ATTR_ENTITY_PICTURE))
self.assertEqual(
'http://example.com/dev2.jpg',
self.hass.states.get(dev2).attributes.get(ATTR_ENTITY_PICTURE))
# Test if dev3 got added to known dev file
with open(self.known_dev_path) as fil:
self.assertEqual('dev3,DEV3,0,\n', list(fil)[-1])
# Change dev3 to track
with open(self.known_dev_path, 'w') as fil:
fil.write("device,name,track,picture\n")
fil.write('dev1,Device 1,1,http://example.com/picture.jpg\n')
fil.write('dev2,Device 2,1,http://example.com/picture.jpg\n')
fil.write('dev3,DEV3,1,\n')
# reload dev file
scanner.come_home('dev1')
scanner.leave_home('dev2')
self.hass.services.call(
device_tracker.DOMAIN,
device_tracker.SERVICE_DEVICE_TRACKER_RELOAD)
self.hass._pool.block_till_done()
# Test what happens if a device comes home and another leaves
self.assertTrue(device_tracker.is_on(self.hass))
self.assertTrue(device_tracker.is_on(self.hass, dev1))
# Dev2 will still be home because of the error margin on time
self.assertTrue(device_tracker.is_on(self.hass, dev2))
# dev3 should be tracked now after we reload the known devices
self.assertTrue(device_tracker.is_on(self.hass, dev3))
self.assertIsNone(
self.hass.states.get(dev3).attributes.get(ATTR_ENTITY_PICTURE))
# Test if device leaves what happens, test the time span
self.hass.bus.fire(
ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: nowAlmostMinGone})
self.hass._pool.block_till_done()
self.assertTrue(device_tracker.is_on(self.hass))
self.assertTrue(device_tracker.is_on(self.hass, dev1))
# Dev2 will still be home because of the error time
self.assertTrue(device_tracker.is_on(self.hass, dev2))
self.assertTrue(device_tracker.is_on(self.hass, dev3))
# Now test if gone for longer then error margin
self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: nowMinGone})
self.hass._pool.block_till_done()
self.assertTrue(device_tracker.is_on(self.hass))
self.assertTrue(device_tracker.is_on(self.hass, dev1))
self.assertFalse(device_tracker.is_on(self.hass, dev2))
self.assertTrue(device_tracker.is_on(self.hass, dev3))

View File

@ -557,6 +557,9 @@ class StateMachine(object):
Track specific state changes. Track specific state changes.
entity_ids, from_state and to_state can be string or list. entity_ids, from_state and to_state can be string or list.
Use list to match multiple. Use list to match multiple.
Returns the listener that listens on the bus for EVENT_STATE_CHANGED.
Pass the return value into hass.bus.remove_listener to remove it.
""" """
from_state = _process_match_param(from_state) from_state = _process_match_param(from_state)
to_state = _process_match_param(to_state) to_state = _process_match_param(to_state)
@ -579,6 +582,8 @@ class StateMachine(object):
self._bus.listen(EVENT_STATE_CHANGED, state_listener) self._bus.listen(EVENT_STATE_CHANGED, state_listener)
return state_listener
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class ServiceCall(object): class ServiceCall(object):

View File

@ -1,6 +1,6 @@
""" """
homeassistant.components.tracker homeassistant.components.tracker
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to keep track of devices. Provides functionality to keep track of devices.
""" """
@ -13,9 +13,9 @@ from datetime import datetime, timedelta
import homeassistant as ha import homeassistant as ha
from homeassistant.loader import get_component from homeassistant.loader import get_component
import homeassistant.util as util import homeassistant.util as util
import homeassistant.components as components
from homeassistant.components import group from homeassistant.components import (
group, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME)
DOMAIN = "device_tracker" DOMAIN = "device_tracker"
DEPENDENCIES = [] DEPENDENCIES = []
@ -30,7 +30,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
# After how much time do we consider a device not home if # After how much time do we consider a device not home if
# it does not show up on scans # it does not show up on scans
TIME_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=3) TIME_DEVICE_NOT_FOUND = timedelta(minutes=3)
# Filename to save known devices to # Filename to save known devices to
KNOWN_DEVICES_FILE = "known_devices.csv" KNOWN_DEVICES_FILE = "known_devices.csv"
@ -43,7 +43,7 @@ def is_on(hass, entity_id=None):
""" Returns if any or specified device is home. """ """ Returns if any or specified device is home. """
entity = entity_id or ENTITY_ID_ALL_DEVICES entity = entity_id or ENTITY_ID_ALL_DEVICES
return hass.states.is_state(entity, components.STATE_HOME) return hass.states.is_state(entity, STATE_HOME)
def setup(hass, config): def setup(hass, config):
@ -70,223 +70,231 @@ def setup(hass, config):
return False return False
DeviceTracker(hass, device_scanner) tracker = DeviceTracker(hass, device_scanner)
return True # We only succeeded if we got to parse the known devices file
return not tracker.invalid_known_devices_file
# pylint: disable=too-many-instance-attributes
class DeviceTracker(object): class DeviceTracker(object):
""" Class that tracks which devices are home and which are not. """ """ Class that tracks which devices are home and which are not. """
def __init__(self, hass, device_scanner): def __init__(self, hass, device_scanner):
self.states = hass.states self.hass = hass
self.device_scanner = device_scanner self.device_scanner = device_scanner
self.error_scanning = TIME_SPAN_FOR_ERROR_IN_SCANNING
self.lock = threading.Lock() self.lock = threading.Lock()
self.path_known_devices_file = hass.get_config_path(KNOWN_DEVICES_FILE)
# Dictionary to keep track of known devices and devices we track # Dictionary to keep track of known devices and devices we track
self.known_devices = {} self.tracked = {}
self.untracked_devices = set()
# Did we encounter an invalid known devices file # Did we encounter an invalid known devices file
self.invalid_known_devices_file = False self.invalid_known_devices_file = False
self._read_known_devices_file() self._read_known_devices_file()
if self.invalid_known_devices_file:
return
# Wrap it in a func instead of lambda so it can be identified in # Wrap it in a func instead of lambda so it can be identified in
# the bus by its __name__ attribute. # the bus by its __name__ attribute.
def update_device_state(time): # pylint: disable=unused-argument def update_device_state(now):
""" Triggers update of the device states. """ """ Triggers update of the device states. """
self.update_devices() self.update_devices(now)
# pylint: disable=unused-argument
def reload_known_devices_service(service):
""" Reload known devices file. """
group.remove_group(self.hass, GROUP_NAME_ALL_DEVICES)
self._read_known_devices_file()
self.update_devices(datetime.now())
if self.tracked:
group.setup_group(
self.hass, GROUP_NAME_ALL_DEVICES,
self.device_entity_ids, False)
hass.track_time_change(update_device_state) hass.track_time_change(update_device_state)
hass.services.register(DOMAIN, hass.services.register(DOMAIN,
SERVICE_DEVICE_TRACKER_RELOAD, SERVICE_DEVICE_TRACKER_RELOAD,
lambda service: self._read_known_devices_file()) reload_known_devices_service)
self.update_devices() reload_known_devices_service(None)
group.setup_group(
hass, GROUP_NAME_ALL_DEVICES, self.device_entity_ids, False)
@property @property
def device_entity_ids(self): def device_entity_ids(self):
""" Returns a set containing all device entity ids """ Returns a set containing all device entity ids
that are being tracked. """ that are being tracked. """
return set([self.known_devices[device]['entity_id'] for device return set(device['entity_id'] for device in self.tracked.values())
in self.known_devices
if self.known_devices[device]['track']])
def update_devices(self, found_devices=None): def _update_state(self, now, device, is_home):
""" Update the state of a device. """
dev_info = self.tracked[device]
if is_home:
# Update last seen if at home
dev_info['last_seen'] = now
else:
# State remains at home if it has been seen in the last
# TIME_DEVICE_NOT_FOUND
is_home = now - dev_info['last_seen'] < TIME_DEVICE_NOT_FOUND
state = STATE_HOME if is_home else STATE_NOT_HOME
self.hass.states.set(
dev_info['entity_id'], state,
dev_info['state_attr'])
def update_devices(self, now):
""" Update device states based on the found devices. """ """ Update device states based on the found devices. """
self.lock.acquire() self.lock.acquire()
found_devices = found_devices or self.device_scanner.scan_devices() found_devices = set(self.device_scanner.scan_devices())
now = datetime.now() for device in self.tracked:
is_home = device in found_devices
known_dev = self.known_devices self._update_state(now, device, is_home)
temp_tracking_devices = [device for device in known_dev if is_home:
if known_dev[device]['track']] found_devices.remove(device)
for device in found_devices: # Did we find any devices that we didn't know about yet?
# Are we tracking this device? new_devices = found_devices - self.untracked_devices
if device in temp_tracking_devices:
temp_tracking_devices.remove(device)
known_dev[device]['last_seen'] = now # Write new devices to known devices file
if not self.invalid_known_devices_file and new_devices:
self.states.set( known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE)
known_dev[device]['entity_id'], components.STATE_HOME,
known_dev[device]['default_state_attr'])
# For all devices we did not find, set state to NH try:
# But only if they have been gone for longer then the error time span # If file does not exist we will write the header too
# Because we do not want to have stuff happening when the device does is_new_file = not os.path.isfile(known_dev_path)
# not show up for 1 scan beacuse of reboot etc
for device in temp_tracking_devices:
if now - known_dev[device]['last_seen'] > self.error_scanning:
self.states.set(known_dev[device]['entity_id'], with open(known_dev_path, 'a') as outp:
components.STATE_NOT_HOME, _LOGGER.info(
known_dev[device]['default_state_attr']) "Found %d new devices, updating %s",
len(new_devices), known_dev_path)
# If we come along any unknown devices we will write them to the writer = csv.writer(outp)
# known devices file but only if we did not encounter an invalid
# known devices file
if not self.invalid_known_devices_file:
known_dev_path = self.path_known_devices_file if is_new_file:
writer.writerow((
"device", "name", "track", "picture"))
unknown_devices = [device for device in found_devices for device in new_devices:
if device not in known_dev] # See if the device scanner knows the name
# else defaults to unknown device
name = (self.device_scanner.get_device_name(device)
or "unknown_device")
if unknown_devices: writer.writerow((device, name, 0, ""))
try:
# If file does not exist we will write the header too
is_new_file = not os.path.isfile(known_dev_path)
with open(known_dev_path, 'a') as outp: except IOError:
_LOGGER.info( _LOGGER.exception(
"Found %d new devices, updating %s", "Error updating %s with %d new devices",
len(unknown_devices), known_dev_path) known_dev_path, len(new_devices))
writer = csv.writer(outp)
if is_new_file:
writer.writerow((
"device", "name", "track", "picture"))
for device in unknown_devices:
# See if the device scanner knows the name
# else defaults to unknown device
name = (self.device_scanner.get_device_name(device)
or "unknown_device")
writer.writerow((device, name, 0, ""))
known_dev[device] = {'name': name,
'track': False,
'picture': ""}
except IOError:
_LOGGER.exception(
"Error updating %s with %d new devices",
known_dev_path, len(unknown_devices))
self.lock.release() self.lock.release()
# pylint: disable=too-many-branches
def _read_known_devices_file(self): def _read_known_devices_file(self):
""" Parse and process the known devices file. """ """ Parse and process the known devices file. """
known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE)
# Read known devices if file exists # Return if no known devices file exists
if os.path.isfile(self.path_known_devices_file): if not os.path.isfile(known_dev_path):
self.lock.acquire() return
known_devices = {} self.lock.acquire()
with open(self.path_known_devices_file) as inp: self.untracked_devices.clear()
default_last_seen = datetime(1990, 1, 1)
# Temp variable to keep track of which entity ids we use with open(known_dev_path) as inp:
# so we can ensure we have unique entity ids. default_last_seen = datetime(1990, 1, 1)
used_entity_ids = []
try: # To track which devices need an entity_id assigned
for row in csv.DictReader(inp): need_entity_id = []
device = row['device']
row['track'] = True if row['track'] == '1' else False # All devices that are still in this set after we read the CSV file
# have been removed from the file and thus need to be cleaned up.
removed_devices = set(self.tracked.keys())
try:
for row in csv.DictReader(inp):
device = row['device']
if row['track'] == '1':
if device in self.tracked:
# Device exists
removed_devices.remove(device)
else:
# We found a new device
need_entity_id.append(device)
self.tracked[device] = {
'name': row['name'],
'last_seen': default_last_seen
}
# Update state_attr with latest from file
state_attr = {
ATTR_FRIENDLY_NAME: row['name']
}
if row['picture']: if row['picture']:
row['default_state_attr'] = { state_attr[ATTR_ENTITY_PICTURE] = row['picture']
components.ATTR_ENTITY_PICTURE: row['picture']}
else: self.tracked[device]['state_attr'] = state_attr
row['default_state_attr'] = None
# If we track this device setup tracking variables else:
if row['track']: self.untracked_devices.add(device)
row['last_seen'] = default_last_seen
# Make sure that each device is mapped # Remove existing devices that we no longer track
# to a unique entity_id name for device in removed_devices:
name = util.slugify(row['name']) if row['name'] \ entity_id = self.tracked[device]['entity_id']
else "unnamed_device"
entity_id = ENTITY_ID_FORMAT.format(name) _LOGGER.info("Removing entity %s", entity_id)
tries = 1
while entity_id in used_entity_ids: self.hass.states.remove(entity_id)
tries += 1
suffix = "_{}".format(tries) self.tracked.pop(device)
entity_id = ENTITY_ID_FORMAT.format( # Setup entity_ids for the new devices
name + suffix) used_entity_ids = [info['entity_id'] for device, info
in self.tracked.items()
if device not in need_entity_id]
row['entity_id'] = entity_id for device in need_entity_id:
used_entity_ids.append(entity_id) name = self.tracked[device]['name']
row['picture'] = row['picture'] entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(name)),
used_entity_ids)
known_devices[device] = row used_entity_ids.append(entity_id)
if not known_devices: self.tracked[device]['entity_id'] = entity_id
_LOGGER.warning(
"No devices to track. Please update %s.",
self.path_known_devices_file)
# Remove entities that are no longer maintained if not self.tracked:
new_entity_ids = set([known_devices[dev]['entity_id']
for dev in known_devices
if known_devices[dev]['track']])
for entity_id in \
self.device_entity_ids - new_entity_ids:
_LOGGER.info("Removing entity %s", entity_id)
self.states.remove(entity_id)
# File parsed, warnings given if necessary
# entities cleaned up, make it available
self.known_devices = known_devices
_LOGGER.info("Loaded devices from %s",
self.path_known_devices_file)
except KeyError:
self.invalid_known_devices_file = True
_LOGGER.warning( _LOGGER.warning(
("Invalid known devices file: %s. " "No devices to track. Please update %s.",
"We won't update it with new found devices."), known_dev_path)
self.path_known_devices_file)
finally: _LOGGER.info("Loaded devices from %s", known_dev_path)
self.lock.release()
except KeyError:
self.invalid_known_devices_file = True
_LOGGER.warning(
("Invalid known devices file: %s. "
"We won't update it with new found devices."),
known_dev_path)
finally:
self.lock.release()

View File

@ -7,6 +7,7 @@ Provides functionality to group devices that can be turned on or off.
import logging import logging
import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
from homeassistant.components import (STATE_ON, STATE_OFF, from homeassistant.components import (STATE_ON, STATE_OFF,
STATE_HOME, STATE_NOT_HOME, STATE_HOME, STATE_NOT_HOME,
@ -24,6 +25,8 @@ _GROUP_TYPES = {
"home_not_home": (STATE_HOME, STATE_NOT_HOME) "home_not_home": (STATE_HOME, STATE_NOT_HOME)
} }
_GROUPS = {}
def _get_group_type(state): def _get_group_type(state):
""" Determine the group type based on the given group type. """ """ Determine the group type based on the given group type. """
@ -105,7 +108,6 @@ def setup(hass, config):
def setup_group(hass, name, entity_ids, user_defined=True): def setup_group(hass, name, entity_ids, user_defined=True):
""" Sets up a group state that is the combined state of """ Sets up a group state that is the combined state of
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """ several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
# In case an iterable is passed in # In case an iterable is passed in
entity_ids = list(entity_ids) entity_ids = list(entity_ids)
@ -159,35 +161,47 @@ def setup_group(hass, name, entity_ids, user_defined=True):
return False return False
else: group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
group_entity_id = ENTITY_ID_FORMAT.format(name) state_attr = {ATTR_ENTITY_ID: entity_ids, ATTR_AUTO: not user_defined}
state_attr = {ATTR_ENTITY_ID: entity_ids, ATTR_AUTO: not user_defined}
# pylint: disable=unused-argument # pylint: disable=unused-argument
def update_group_state(entity_id, old_state, new_state): def update_group_state(entity_id, old_state, new_state):
""" Updates the group state based on a state change by """ Updates the group state based on a state change by
a tracked entity. """ a tracked entity. """
cur_gr_state = hass.states.get(group_entity_id).state cur_gr_state = hass.states.get(group_entity_id).state
# if cur_gr_state = OFF and new_state = ON: set ON # if cur_gr_state = OFF and new_state = ON: set ON
# if cur_gr_state = ON and new_state = OFF: research # if cur_gr_state = ON and new_state = OFF: research
# else: ignore # else: ignore
if cur_gr_state == group_off and new_state.state == group_on: if cur_gr_state == group_off and new_state.state == group_on:
hass.states.set(group_entity_id, group_on, state_attr) hass.states.set(group_entity_id, group_on, state_attr)
elif cur_gr_state == group_on and new_state.state == group_off: elif cur_gr_state == group_on and new_state.state == group_off:
# Check if any of the other states is still on # Check if any of the other states is still on
if not any([hass.states.is_state(ent_id, group_on) if not any([hass.states.is_state(ent_id, group_on)
for ent_id in entity_ids for ent_id in entity_ids
if entity_id != ent_id]): if entity_id != ent_id]):
hass.states.set(group_entity_id, group_off, state_attr) hass.states.set(group_entity_id, group_off, state_attr)
hass.states.track_change(entity_ids, update_group_state) _GROUPS[group_entity_id] = hass.states.track_change(
entity_ids, update_group_state)
hass.states.set(group_entity_id, group_state, state_attr) hass.states.set(group_entity_id, group_state, state_attr)
return True return True
def remove_group(hass, name):
""" Remove a group and its state listener from Home Assistant. """
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
if hass.states.get(group_entity_id) is not None:
hass.states.remove(group_entity_id)
if group_entity_id in _GROUPS:
hass.bus.remove_listener(
ha.EVENT_STATE_CHANGED, _GROUPS.pop(group_entity_id))