RENAME: StateMachine tracks now entities, not categories.

This commit is contained in:
Paulus Schoutsen 2014-01-19 23:37:40 -08:00
parent e7f5953362
commit e9e1b007ed
13 changed files with 236 additions and 236 deletions

View File

@ -62,11 +62,11 @@ Other status codes that can occur are:
The api supports the following actions: The api supports the following actions:
**/api/states - GET**<br> **/api/states - GET**<br>
Returns a list of categories for which a state is available Returns a list of entity ids for which a state is available
```json ```json
{ {
"categories": [ "entity_ids": [
"Paulus_Nexus_4", "Paulus_Nexus_4",
"weather.sun", "weather.sun",
"all_devices" "all_devices"
@ -103,8 +103,8 @@ Returns a dict with as keys the domain and as value a list of published services
} }
``` ```
**/api/states/&lt;category>** - GET<br> **/api/states/&lt;entity_id>** - GET<br>
Returns the current state from a category Returns the current state from an entity
```json ```json
{ {
@ -112,14 +112,14 @@ Returns the current state from a category
"next_rising": "07:04:15 29-10-2013", "next_rising": "07:04:15 29-10-2013",
"next_setting": "18:00:31 29-10-2013" "next_setting": "18:00:31 29-10-2013"
}, },
"category": "weather.sun", "entity_id": "weather.sun",
"last_changed": "23:24:33 28-10-2013", "last_changed": "23:24:33 28-10-2013",
"state": "below_horizon" "state": "below_horizon"
} }
``` ```
**/api/states/&lt;category>** - POST<br> **/api/states/&lt;entity_id>** - POST<br>
Updates the current state of a category. Returns status code 201 if successful with location header of updated resource and the new state in the body.<br> Updates the current state of an entity. Returns status code 201 if successful with location header of updated resource and the new state in the body.<br>
parameter: new_state - string<br> parameter: new_state - string<br>
optional parameter: attributes - JSON encoded object optional parameter: attributes - JSON encoded object
@ -129,7 +129,7 @@ optional parameter: attributes - JSON encoded object
"next_rising": "07:04:15 29-10-2013", "next_rising": "07:04:15 29-10-2013",
"next_setting": "18:00:31 29-10-2013" "next_setting": "18:00:31 29-10-2013"
}, },
"category": "weather.sun", "entity_id": "weather.sun",
"last_changed": "23:24:33 28-10-2013", "last_changed": "23:24:33 28-10-2013",
"state": "below_horizon" "state": "below_horizon"
} }

View File

@ -3,13 +3,13 @@ homeassistant
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
Home Assistant is a Home Automation framework for observing the state Home Assistant is a Home Automation framework for observing the state
of objects and react to changes. of entities and react to changes.
""" """
import time import time
import logging import logging
import threading import threading
from collections import defaultdict, namedtuple from collections import namedtuple
import datetime as dt import datetime as dt
import homeassistant.util as util import homeassistant.util as util
@ -78,33 +78,33 @@ def _matcher(subject, pattern):
return MATCH_ALL == pattern or subject in pattern return MATCH_ALL == pattern or subject in pattern
def split_state_category(category): def split_entity_id(entity_id):
""" Splits a state category into domain, object_id. """ """ Splits a state entity_id into domain, object_id. """
return category.split(".", 1) return entity_id.split(".", 1)
def filter_categories(categories, domain_filter=None, strip_domain=False): def filter_entity_ids(entity_ids, domain_filter=None, strip_domain=False):
""" Filter a list of categories based on domain. Setting strip_domain """ Filter a list of entities based on domain. Setting strip_domain
will only return the object_ids. """ will only return the object_ids. """
return [ return [
split_state_category(cat)[1] if strip_domain else cat split_entity_id(entity_id)[1] if strip_domain else entity_id
for cat in categories if for entity_id in entity_ids if
not domain_filter or cat.startswith(domain_filter) not domain_filter or entity_id.startswith(domain_filter)
] ]
def track_state_change(bus, category, action, from_state=None, to_state=None): def track_state_change(bus, entity_id, action, from_state=None, to_state=None):
""" Helper method to track specific state changes. """ """ Helper method to track specific state changes. """
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)
def listener(event): def listener(event):
""" State change listener that listens for specific state changes. """ """ State change listener that listens for specific state changes. """
if category == event.data['category'] and \ if entity_id == event.data['entity_id'] and \
_matcher(event.data['old_state'].state, from_state) and \ _matcher(event.data['old_state'].state, from_state) and \
_matcher(event.data['new_state'].state, to_state): _matcher(event.data['new_state'].state, to_state):
action(event.data['category'], action(event.data['entity_id'],
event.data['old_state'], event.data['old_state'],
event.data['new_state']) event.data['new_state'])
@ -304,7 +304,11 @@ class State(object):
else: else:
self.last_changed = last_changed self.last_changed = last_changed
def to_json_dict(self, category=None): def copy(self):
""" Creates a copy of itself. """
return State(self.state, dict(self.attributes), self.last_changed)
def to_json_dict(self, entity_id=None):
""" Converts State to a dict to be used within JSON. """ Converts State to a dict to be used within JSON.
Ensures: state == State.from_json_dict(state.to_json_dict()) """ Ensures: state == State.from_json_dict(state.to_json_dict()) """
@ -312,15 +316,11 @@ class State(object):
'attributes': self.attributes, 'attributes': self.attributes,
'last_changed': util.datetime_to_str(self.last_changed)} 'last_changed': util.datetime_to_str(self.last_changed)}
if category: if entity_id:
json_dict['category'] = category json_dict['entity_id'] = entity_id
return json_dict return json_dict
def copy(self):
""" Creates a copy of itself. """
return State(self.state, dict(self.attributes), self.last_changed)
@staticmethod @staticmethod
def from_json_dict(json_dict): def from_json_dict(json_dict):
""" Static method to create a state from a dict. """ Static method to create a state from a dict.
@ -345,75 +345,75 @@ class State(object):
class StateMachine(object): class StateMachine(object):
""" Helper class that tracks the state of different categories. """ """ Helper class that tracks the state of different entities. """
def __init__(self, bus): def __init__(self, bus):
self.states = dict() self.states = {}
self.bus = bus self.bus = bus
self.lock = threading.Lock() self.lock = threading.Lock()
@property @property
def categories(self): def entity_ids(self):
""" List of categories which states are being tracked. """ """ List of entitie ids that are being tracked. """
with self.lock: with self.lock:
return self.states.keys() return self.states.keys()
def remove_category(self, category): def remove_entity(self, entity_id):
""" Removes a category from the state machine. """ Removes a entity from the state machine.
Returns boolean to indicate if a category was removed. """ Returns boolean to indicate if a entity was removed. """
with self.lock: with self.lock:
try: try:
del self.states[category] del self.states[entity_id]
return True return True
except KeyError: except KeyError:
# if category does not exist # if entity does not exist
return False return False
def set_state(self, category, new_state, attributes=None): def set_state(self, entity_id, new_state, attributes=None):
""" Set the state of a category, add category if it does not exist. """ Set the state of an entity, add entity if it does not exist.
Attributes is an optional dict to specify attributes of this state. """ Attributes is an optional dict to specify attributes of this state. """
attributes = attributes or {} attributes = attributes or {}
with self.lock: with self.lock:
# Add category if it does not exist # Add entity if it does not exist
if category not in self.states: if entity_id not in self.states:
self.states[category] = State(new_state, attributes) self.states[entity_id] = State(new_state, attributes)
# Change state and fire listeners # Change state and fire listeners
else: else:
old_state = self.states[category] old_state = self.states[entity_id]
if old_state.state != new_state or \ if old_state.state != new_state or \
old_state.attributes != attributes: old_state.attributes != attributes:
self.states[category] = State(new_state, attributes) self.states[entity_id] = State(new_state, attributes)
self.bus.fire_event(EVENT_STATE_CHANGED, self.bus.fire_event(EVENT_STATE_CHANGED,
{'category': category, {'entity_id': entity_id,
'old_state': old_state, 'old_state': old_state,
'new_state': self.states[category]}) 'new_state': self.states[entity_id]})
def get_state(self, category): def get_state(self, entity_id):
""" Returns a dict (state, last_changed, attributes) describing """ Returns a dict (state, last_changed, attributes) describing
the state of the specified category. """ the state of the specified entity. """
with self.lock: with self.lock:
try: try:
# Make a copy so people won't mutate the state # Make a copy so people won't mutate the state
return self.states[category].copy() return self.states[entity_id].copy()
except KeyError: except KeyError:
# If category does not exist # If entity does not exist
return None return None
def is_state(self, category, state): def is_state(self, entity_id, state):
""" Returns True if category exists and is specified state. """ """ Returns True if entity exists and is specified state. """
try: try:
return self.get_state(category).state == state return self.get_state(entity_id).state == state
except AttributeError: except AttributeError:
# get_state returned None # get_state returned None
return False return False
@ -438,8 +438,10 @@ class Timer(threading.Thread):
last_fired_on_second = -1 last_fired_on_second = -1
calc_now = dt.datetime.now
while True: while True:
now = dt.datetime.now() now = calc_now()
# First check checks if we are not on a second matching the # First check checks if we are not on a second matching the
# timer interval. Second check checks if we did not already fire # timer interval. Second check checks if we did not already fire
@ -457,7 +459,7 @@ class Timer(threading.Thread):
time.sleep(slp_seconds) time.sleep(slp_seconds)
now = dt.datetime.now() now = calc_now()
last_fired_on_second = now.second last_fired_on_second = now.second

View File

@ -138,10 +138,10 @@ def from_config_file(config_path):
# Init groups # Init groups
if has_section("groups"): if has_section("groups"):
for name, categories in config.items("groups"): for name, entity_ids in config.items("groups"):
add_status("Group - {}".format(name), add_status("Group - {}".format(name),
group.setup(bus, statemachine, name, group.setup(bus, statemachine, name,
categories.split(","))) entity_ids.split(",")))
# Light trigger # Light trigger
if light_control: if light_control:

View File

@ -8,7 +8,7 @@ Component design guidelines:
Each component defines a constant DOMAIN that is equal to its filename. Each component defines a constant DOMAIN that is equal to its filename.
Each component that tracks states should create state category names in the Each component that tracks states should create state entity names in the
format "<DOMAIN>.<OBJECT_ID>". format "<DOMAIN>.<OBJECT_ID>".
Each component should publish services only under its own domain. Each component should publish services only under its own domain.

View File

@ -16,7 +16,7 @@ DOMAIN = "chromecast"
SERVICE_YOUTUBE_VIDEO = "play_youtube_video" SERVICE_YOUTUBE_VIDEO = "play_youtube_video"
STATE_CATEGORY_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
STATE_NO_APP = "none" STATE_NO_APP = "none"
ATTR_FRIENDLY_NAME = "friendly_name" ATTR_FRIENDLY_NAME = "friendly_name"
@ -29,15 +29,15 @@ def turn_off(statemachine, cc_id=None):
""" Exits any running app on the specified ChromeCast and shows """ Exits any running app on the specified ChromeCast and shows
idle screen. Will quit all ChromeCasts if nothing specified. """ idle screen. Will quit all ChromeCasts if nothing specified. """
cats = [STATE_CATEGORY_FORMAT.format(cc_id)] if cc_id \ entity_ids = [ENTITY_ID_FORMAT.format(cc_id)] if cc_id \
else ha.filter_categories(statemachine.categories, DOMAIN) else ha.filter_entity_ids(statemachine.entity_ids, DOMAIN)
for cat in cats: for entity_id in entity_ids:
state = statemachine.get_state(cat) state = statemachine.get_state(entity_id)
if state and \ if (state and
state.state != STATE_NO_APP or \ (state.state != STATE_NO_APP or
state.state != pychromecast.APP_ID_HOME: state.state != pychromecast.APP_ID_HOME)):
pychromecast.quit_app(state.attributes[ATTR_HOST]) pychromecast.quit_app(state.attributes[ATTR_HOST])
@ -53,7 +53,7 @@ def setup(bus, statemachine, host):
logger.error("Could not find Chromecast") logger.error("Could not find Chromecast")
return False return False
category = STATE_CATEGORY_FORMAT.format(util.slugify( entity = ENTITY_ID_FORMAT.format(util.slugify(
device.friendly_name)) device.friendly_name))
bus.register_service(DOMAIN, ha.SERVICE_TURN_OFF, bus.register_service(DOMAIN, ha.SERVICE_TURN_OFF,
@ -80,7 +80,7 @@ def setup(bus, statemachine, host):
status = pychromecast.get_app_status(host) status = pychromecast.get_app_status(host)
if status: if status:
statemachine.set_state(category, status.name, statemachine.set_state(entity, status.name,
{ATTR_FRIENDLY_NAME: {ATTR_FRIENDLY_NAME:
pychromecast.get_friendly_name( pychromecast.get_friendly_name(
status.name), status.name),
@ -88,7 +88,7 @@ def setup(bus, statemachine, host):
ATTR_STATE: status.state, ATTR_STATE: status.state,
ATTR_OPTIONS: status.options}) ATTR_OPTIONS: status.options})
else: else:
statemachine.set_state(category, STATE_NO_APP, {ATTR_HOST: host}) statemachine.set_state(entity, STATE_NO_APP, {ATTR_HOST: host})
ha.track_time_change(bus, update_chromecast_state) ha.track_time_change(bus, update_chromecast_state)

View File

@ -22,20 +22,20 @@ def setup(bus, statemachine, light_group=None):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
device_state_categories = ha.filter_categories(statemachine.categories, device_entity_ids = ha.filter_entity_ids(statemachine.entity_ids,
device_tracker.DOMAIN) device_tracker.DOMAIN)
if not device_state_categories: if not device_entity_ids:
logger.error("LightTrigger:No devices found to track") logger.error("LightTrigger:No devices found to track")
return False return False
if not light_group: if not light_group:
light_group = light.STATE_GROUP_NAME_ALL_LIGHTS light_group = light.GROUP_NAME_ALL_LIGHTS
# Get the light IDs from the specified group # Get the light IDs from the specified group
light_ids = ha.filter_categories( light_ids = ha.filter_entity_ids(
group.get_categories(statemachine, light_group), light.DOMAIN, True) group.get_entity_ids(statemachine, light_group), light.DOMAIN, True)
if not light_ids: if not light_ids:
logger.error("LightTrigger:No lights found to turn on ") logger.error("LightTrigger:No lights found to turn on ")
@ -48,7 +48,7 @@ def setup(bus, statemachine, light_group=None):
len(light_ids)) len(light_ids))
# pylint: disable=unused-argument # pylint: disable=unused-argument
def handle_sun_rising(category, old_state, new_state): def handle_sun_rising(entity, old_state, new_state):
"""The moment sun sets we want to have all the lights on. """The moment sun sets we want to have all the lights on.
We will schedule to have each light start after one another We will schedule to have each light start after one another
and slowly transition in.""" and slowly transition in."""
@ -76,7 +76,7 @@ def setup(bus, statemachine, light_group=None):
# Track every time sun rises so we can schedule a time-based # Track every time sun rises so we can schedule a time-based
# pre-sun set event # pre-sun set event
ha.track_state_change(bus, sun.STATE_CATEGORY, handle_sun_rising, ha.track_state_change(bus, sun.ENTITY_ID, handle_sun_rising,
sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
# If the sun is already above horizon # If the sun is already above horizon
@ -84,14 +84,14 @@ def setup(bus, statemachine, light_group=None):
if sun.is_up(statemachine): if sun.is_up(statemachine):
handle_sun_rising(None, None, None) handle_sun_rising(None, None, None)
def handle_device_state_change(category, old_state, new_state): def handle_device_state_change(entity, old_state, new_state):
""" Function to handle tracked device state changes. """ """ Function to handle tracked device state changes. """
lights_are_on = group.is_on(statemachine, light_group) lights_are_on = group.is_on(statemachine, light_group)
light_needed = not (lights_are_on or sun.is_up(statemachine)) light_needed = not (lights_are_on or sun.is_up(statemachine))
# Specific device came home ? # Specific device came home ?
if (category != device_tracker.STATE_CATEGORY_ALL_DEVICES and if (entity != device_tracker.ENTITY_ID_ALL_DEVICES and
new_state.state == ha.STATE_HOME): new_state.state == ha.STATE_HOME):
# These variables are needed for the elif check # These variables are needed for the elif check
@ -103,7 +103,7 @@ def setup(bus, statemachine, light_group=None):
logger.info( logger.info(
"Home coming event for {}. Turning lights on". "Home coming event for {}. Turning lights on".
format(category)) format(entity))
for light_id in light_ids: for light_id in light_ids:
light.turn_on(bus, light_id) light.turn_on(bus, light_id)
@ -127,7 +127,7 @@ def setup(bus, statemachine, light_group=None):
break break
# Did all devices leave the house? # Did all devices leave the house?
elif (category == device_tracker.STATE_CATEGORY_ALL_DEVICES and elif (entity == device_tracker.ENTITY_ID_ALL_DEVICES and
new_state.state == ha.STATE_NOT_HOME and lights_are_on): new_state.state == ha.STATE_NOT_HOME and lights_are_on):
logger.info( logger.info(
@ -136,12 +136,12 @@ def setup(bus, statemachine, light_group=None):
general.shutdown_devices(bus, statemachine) general.shutdown_devices(bus, statemachine)
# Track home coming of each seperate device # Track home coming of each seperate device
for category in device_state_categories: for entity in device_entity_ids:
ha.track_state_change(bus, category, handle_device_state_change, ha.track_state_change(bus, entity, handle_device_state_change,
ha.STATE_NOT_HOME, ha.STATE_HOME) ha.STATE_NOT_HOME, ha.STATE_HOME)
# Track when all devices are gone to shut down lights # Track when all devices are gone to shut down lights
ha.track_state_change(bus, device_tracker.STATE_CATEGORY_ALL_DEVICES, ha.track_state_change(bus, device_tracker.ENTITY_ID_ALL_DEVICES,
handle_device_state_change, ha.STATE_HOME, handle_device_state_change, ha.STATE_HOME,
ha.STATE_NOT_HOME) ha.STATE_NOT_HOME)

View File

@ -24,11 +24,11 @@ DOMAIN = "device_tracker"
SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv" SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv"
STATE_GROUP_NAME_ALL_DEVICES = 'all_tracked_devices' GROUP_NAME_ALL_DEVICES = 'all_tracked_devices'
STATE_CATEGORY_ALL_DEVICES = group.STATE_CATEGORY_FORMAT.format( ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format(
STATE_GROUP_NAME_ALL_DEVICES) GROUP_NAME_ALL_DEVICES)
STATE_CATEGORY_FORMAT = DOMAIN + '.{}' 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
@ -43,10 +43,10 @@ KNOWN_DEVICES_FILE = "known_devices.csv"
def is_home(statemachine, device_id=None): def is_home(statemachine, device_id=None):
""" Returns if any or specified device is home. """ """ Returns if any or specified device is home. """
category = STATE_CATEGORY_FORMAT.format(device_id) if device_id \ entity = ENTITY_ID_FORMAT.format(device_id) if device_id \
else STATE_CATEGORY_ALL_DEVICES else ENTITY_ID_ALL_DEVICES
return statemachine.is_state(category, ha.STATE_HOME) return statemachine.is_state(entity, ha.STATE_HOME)
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
@ -83,14 +83,14 @@ class DeviceTracker(object):
self.update_devices(device_scanner.scan_devices()) self.update_devices(device_scanner.scan_devices())
group.setup(bus, statemachine, STATE_GROUP_NAME_ALL_DEVICES, group.setup(bus, statemachine, GROUP_NAME_ALL_DEVICES,
list(self.device_state_categories)) list(self.device_entity_ids))
@property @property
def device_state_categories(self): def device_entity_ids(self):
""" Returns a set containing all categories """ Returns a set containing all device entity ids
that are maintained for devices. """ that are being tracked. """
return set([self.known_devices[device]['category'] for device return set([self.known_devices[device]['entity_id'] for device
in self.known_devices in self.known_devices
if self.known_devices[device]['track']]) if self.known_devices[device]['track']])
@ -111,7 +111,7 @@ class DeviceTracker(object):
self.known_devices[device]['last_seen'] = now self.known_devices[device]['last_seen'] = now
self.statemachine.set_state( self.statemachine.set_state(
self.known_devices[device]['category'], ha.STATE_HOME) self.known_devices[device]['entity_id'], ha.STATE_HOME)
# For all devices we did not find, set state to NH # For all devices we did not find, set state to NH
# But only if they have been gone for longer then the error time span # But only if they have been gone for longer then the error time span
@ -122,7 +122,7 @@ class DeviceTracker(object):
self.error_scanning): self.error_scanning):
self.statemachine.set_state( self.statemachine.set_state(
self.known_devices[device]['category'], self.known_devices[device]['entity_id'],
ha.STATE_NOT_HOME) ha.STATE_NOT_HOME)
# If we come along any unknown devices we will write them to the # If we come along any unknown devices we will write them to the
@ -180,9 +180,9 @@ class DeviceTracker(object):
with open(KNOWN_DEVICES_FILE) as inp: with open(KNOWN_DEVICES_FILE) as inp:
default_last_seen = datetime(1990, 1, 1) default_last_seen = datetime(1990, 1, 1)
# Temp variable to keep track of which categories we use # Temp variable to keep track of which entity ids we use
# so we can ensure we have unique categories. # so we can ensure we have unique entity ids.
used_categories = [] used_entity_ids = []
try: try:
for row in csv.DictReader(inp): for row in csv.DictReader(inp):
@ -195,23 +195,23 @@ class DeviceTracker(object):
row['last_seen'] = default_last_seen row['last_seen'] = default_last_seen
# Make sure that each device is mapped # Make sure that each device is mapped
# to a unique category name # to a unique entity_id name
name = util.slugify(row['name']) if row['name'] \ name = util.slugify(row['name']) if row['name'] \
else "unnamed_device" else "unnamed_device"
category = STATE_CATEGORY_FORMAT.format(name) entity_id = ENTITY_ID_FORMAT.format(name)
tries = 1 tries = 1
while category in used_categories: while entity_id in used_entity_ids:
tries += 1 tries += 1
suffix = "_{}".format(tries) suffix = "_{}".format(tries)
category = STATE_CATEGORY_FORMAT.format( entity_id = ENTITY_ID_FORMAT.format(
name + suffix) name + suffix)
row['category'] = category row['entity_id'] = entity_id
used_categories.append(category) used_entity_ids.append(entity_id)
known_devices[device] = row known_devices[device] = row
@ -220,21 +220,21 @@ class DeviceTracker(object):
"No devices to track. Please update {}.".format( "No devices to track. Please update {}.".format(
KNOWN_DEVICES_FILE)) KNOWN_DEVICES_FILE))
# Remove categories that are no longer maintained # Remove entities that are no longer maintained
new_categories = set([known_devices[device]['category'] new_entity_ids = set([known_devices[device]['entity_id']
for device in known_devices for device in known_devices
if known_devices[device]['track']]) if known_devices[device]['track']])
for category in \ for entity_id in \
self.device_state_categories - new_categories: self.device_entity_ids - new_entity_ids:
self.logger.info( self.logger.info(
"DeviceTracker:Removing category {}".format( "DeviceTracker:Removing entity {}".format(
category)) entity_id))
self.statemachine.remove_category(category) self.statemachine.remove_entity(entity_id)
# File parsed, warnings given if necessary # File parsed, warnings given if necessary
# categories cleaned up, make it available # entities cleaned up, make it available
self.known_devices = known_devices self.known_devices = known_devices
self.logger.info( self.logger.info(

View File

@ -11,9 +11,9 @@ import homeassistant as ha
DOMAIN = "group" DOMAIN = "group"
STATE_CATEGORY_FORMAT = DOMAIN + ".{}" ENTITY_ID_FORMAT = DOMAIN + ".{}"
STATE_ATTR_CATEGORIES = "categories" STATE_ATTR_ENTITY_IDS = "entity_ids"
_GROUP_TYPES = { _GROUP_TYPES = {
"on_off": (ha.STATE_ON, ha.STATE_OFF), "on_off": (ha.STATE_ON, ha.STATE_OFF),
@ -46,29 +46,32 @@ def is_on(statemachine, group):
return False return False
def get_categories(statemachine, group): def get_entity_ids(statemachine, group):
""" Get the categories that make up this group. """ """ Get the entity ids that make up this group. """
state = statemachine.get_state(group) try:
return statemachine.get_state(group).attributes[STATE_ATTR_ENTITY_IDS]
return state.attributes[STATE_ATTR_CATEGORIES] if state else [] except (AttributeError, KeyError):
# AttributeError if state did not exist
# KeyError if key did not exist in attributes
return []
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
def setup(bus, statemachine, name, categories): def setup(bus, statemachine, name, entity_ids):
""" 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. """
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Loop over the given categories to: # Loop over the given entities to:
# - determine which group type this is (on_off, device_home) # - determine which group type this is (on_off, device_home)
# - if all states exist and have valid states # - if all states exist and have valid states
# - retrieve the current state of the group # - retrieve the current state of the group
errors = [] errors = []
group_type, group_on, group_off, group_state = None, None, None, None group_type, group_on, group_off, group_state = None, None, None, None
for cat in categories: for entity_id in entity_ids:
state = statemachine.get_state(cat) state = statemachine.get_state(entity_id)
# Try to determine group type if we didn't yet # Try to determine group type if we didn't yet
if not group_type and state: if not group_type and state:
@ -85,15 +88,15 @@ def setup(bus, statemachine, name, categories):
break break
# Check if category exists # Check if entity exists
if not state: if not state:
errors.append("Category {} does not exist".format(cat)) errors.append("Entity {} does not exist".format(entity_id))
# Check if category is valid state # Check if entity is valid state
elif state.state != group_off and state.state != group_on: elif state.state != group_off and state.state != group_on:
errors.append("State of {} is {} (expected: {}, {})".format( errors.append("State of {} is {} (expected: {}, {})".format(
cat, state.state, group_off, group_on)) entity_id, state.state, group_off, group_on))
# Keep track of the group state to init later on # Keep track of the group state to init later on
elif group_state == group_off and state.state == group_on: elif group_state == group_off and state.state == group_on:
@ -105,15 +108,15 @@ def setup(bus, statemachine, name, categories):
return False return False
group_cat = STATE_CATEGORY_FORMAT.format(name) group_entity_id = ENTITY_ID_FORMAT.format(name)
state_attr = {STATE_ATTR_CATEGORIES: categories} state_attr = {STATE_ATTR_ENTITY_IDS: entity_ids}
# pylint: disable=unused-argument # pylint: disable=unused-argument
def _update_group_state(category, old_state, new_state): def _update_group_state(entity_id, old_state, new_state):
""" Updates the group state based on a state change by a tracked """ Updates the group state based on a state change by a tracked
category. """ entity. """
cur_group_state = statemachine.get_state(group_cat).state cur_group_state = statemachine.get_state(group_entity_id).state
# if cur_group_state = OFF and new_state = ON: set ON # if cur_group_state = OFF and new_state = ON: set ON
# if cur_group_state = ON and new_state = OFF: research # if cur_group_state = ON and new_state = OFF: research
@ -121,18 +124,18 @@ def setup(bus, statemachine, name, categories):
if cur_group_state == group_off and new_state.state == group_on: if cur_group_state == group_off and new_state.state == group_on:
statemachine.set_state(group_cat, group_on, state_attr) statemachine.set_state(group_entity_id, group_on, state_attr)
elif cur_group_state == group_on and new_state.state == group_off: elif cur_group_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([statemachine.is_state(cat, group_on) if not any([statemachine.is_state(ent_id, group_on)
for cat in categories if cat != category]): for ent_id in entity_ids if entity_id != ent_id]):
statemachine.set_state(group_cat, group_off, state_attr) statemachine.set_state(group_entity_id, group_off, state_attr)
for cat in categories: for entity_id in entity_ids:
ha.track_state_change(bus, cat, _update_group_state) ha.track_state_change(bus, entity_id, _update_group_state)
statemachine.set_state(group_cat, group_state, state_attr) statemachine.set_state(group_entity_id, group_state, state_attr)
return True return True

View File

@ -18,31 +18,31 @@ Other status codes that can occur are:
The api supports the following actions: The api supports the following actions:
/api/states - GET /api/states - GET
Returns a list of categories for which a state is available Returns a list of entities for which a state is available
Example result: Example result:
{ {
"categories": [ "entity_ids": [
"Paulus_Nexus_4", "Paulus_Nexus_4",
"weather.sun", "weather.sun",
"all_devices" "all_devices"
] ]
} }
/api/states/<category> - GET /api/states/<entity_id> - GET
Returns the current state from a category Returns the current state from an entity
Example result: Example result:
{ {
"attributes": { "attributes": {
"next_rising": "07:04:15 29-10-2013", "next_rising": "07:04:15 29-10-2013",
"next_setting": "18:00:31 29-10-2013" "next_setting": "18:00:31 29-10-2013"
}, },
"category": "weather.sun", "entity_id": "weather.sun",
"last_changed": "23:24:33 28-10-2013", "last_changed": "23:24:33 28-10-2013",
"state": "below_horizon" "state": "below_horizon"
} }
/api/states/<category> - POST /api/states/<entity_id> - POST
Updates the current state of a category. Returns status code 201 if successful Updates the current state of an entity. Returns status code 201 if successful
with location header of updated resource and as body the new state. with location header of updated resource and as body the new state.
parameter: new_state - string parameter: new_state - string
optional parameter: attributes - JSON encoded object optional parameter: attributes - JSON encoded object
@ -52,7 +52,7 @@ Example result:
"next_rising": "07:04:15 29-10-2013", "next_rising": "07:04:15 29-10-2013",
"next_setting": "18:00:31 29-10-2013" "next_setting": "18:00:31 29-10-2013"
}, },
"category": "weather.sun", "entity_id": "weather.sun",
"last_changed": "23:24:33 28-10-2013", "last_changed": "23:24:33 28-10-2013",
"state": "below_horizon" "state": "below_horizon"
} }
@ -94,7 +94,7 @@ URL_CHANGE_STATE = "/change_state"
URL_FIRE_EVENT = "/fire_event" URL_FIRE_EVENT = "/fire_event"
URL_API_STATES = "/api/states" URL_API_STATES = "/api/states"
URL_API_STATES_CATEGORY = "/api/states/{}" URL_API_STATES_ENTITY = "/api/states/{}"
URL_API_EVENTS = "/api/events" URL_API_EVENTS = "/api/events"
URL_API_EVENTS_EVENT = "/api/events/{}" URL_API_EVENTS_EVENT = "/api/events/{}"
URL_API_SERVICES = "/api/services" URL_API_SERVICES = "/api/services"
@ -150,10 +150,10 @@ class RequestHandler(BaseHTTPRequestHandler):
# /states # /states
('GET', '/api/states', '_handle_get_api_states'), ('GET', '/api/states', '_handle_get_api_states'),
('GET', ('GET',
re.compile(r'/api/states/(?P<category>[a-zA-Z\._0-9]+)'), re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
'_handle_get_api_states_category'), '_handle_get_api_states_entity'),
('POST', ('POST',
re.compile(r'/api/states/(?P<category>[a-zA-Z\._0-9]+)'), re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
'_handle_change_state'), '_handle_change_state'),
# /events # /events
@ -317,8 +317,6 @@ class RequestHandler(BaseHTTPRequestHandler):
self.server.flash_message = None self.server.flash_message = None
# Describe state machine: # Describe state machine:
categories = []
write(("<div class='row'>" write(("<div class='row'>"
"<div class='col-xs-12'>" "<div class='col-xs-12'>"
"<div class='panel panel-primary'>" "<div class='panel panel-primary'>"
@ -328,17 +326,15 @@ class RequestHandler(BaseHTTPRequestHandler):
"class='form-change-state'>" "class='form-change-state'>"
"<input type='hidden' name='api_password' value='{}'>" "<input type='hidden' name='api_password' value='{}'>"
"<table class='table'><tr>" "<table class='table'><tr>"
"<th>Category</th><th>State</th>" "<th>Entity ID</th><th>State</th>"
"<th>Attributes</th><th>Last Changed</th>" "<th>Attributes</th><th>Last Changed</th>"
"</tr>").format(self.server.api_password)) "</tr>").format(self.server.api_password))
for category in \ for entity_id in \
sorted(self.server.statemachine.categories, sorted(self.server.statemachine.entity_ids,
key=lambda key: key.lower()): key=lambda key: key.lower()):
categories.append(category) state = self.server.statemachine.get_state(entity_id)
state = self.server.statemachine.get_state(category)
attributes = "<br>".join( attributes = "<br>".join(
["{}: {}".format(attr, state.attributes[attr]) ["{}: {}".format(attr, state.attributes[attr])
@ -347,14 +343,14 @@ class RequestHandler(BaseHTTPRequestHandler):
write(("<tr>" write(("<tr>"
"<td>{}</td><td>{}</td><td>{}</td><td>{}</td>" "<td>{}</td><td>{}</td><td>{}</td><td>{}</td>"
"</tr>").format( "</tr>").format(
category, entity_id,
state.state, state.state,
attributes, attributes,
util.datetime_to_str(state.last_changed))) util.datetime_to_str(state.last_changed)))
# Change state form # Change state form
write(("<tr><td><input name='category' class='form-control' " write(("<tr><td><input name='entity_id' class='form-control' "
"placeholder='Category'></td>" "placeholder='Entity ID'></td>"
"<td><input name='new_state' class='form-control' " "<td><input name='new_state' class='form-control' "
"placeholder='New State'></td>" "placeholder='New State'></td>"
"<td><textarea rows='3' name='attributes' class='form-control' " "<td><textarea rows='3' name='attributes' class='form-control' "
@ -488,18 +484,18 @@ class RequestHandler(BaseHTTPRequestHandler):
# pylint: disable=invalid-name # pylint: disable=invalid-name
def _handle_change_state(self, path_match, data): def _handle_change_state(self, path_match, data):
""" Handles updating the state of a category. """ Handles updating the state of an entity.
This handles the following paths: This handles the following paths:
/change_state /change_state
/api/states/<category> /api/states/<entity_id>
""" """
try: try:
try: try:
category = path_match.group('category') entity_id = path_match.group('entity_id')
except IndexError: except IndexError:
# If group 'category' does not exist in path_match # If group 'entity_id' does not exist in path_match
category = data['category'][0] entity_id = data['entity_id'][0]
new_state = data['new_state'][0] new_state = data['new_state'][0]
@ -510,21 +506,21 @@ class RequestHandler(BaseHTTPRequestHandler):
attributes = None attributes = None
# Write state # Write state
self.server.statemachine.set_state(category, self.server.statemachine.set_state(entity_id,
new_state, new_state,
attributes) attributes)
# Return state if json, else redirect to main page # Return state if json, else redirect to main page
if self.use_json: if self.use_json:
state = self.server.statemachine.get_state(category) state = self.server.statemachine.get_state(entity_id)
self._write_json(state.to_json_dict(category), self._write_json(state.to_json_dict(entity_id),
status_code=HTTP_CREATED, status_code=HTTP_CREATED,
location= location=
URL_API_STATES_CATEGORY.format(category)) URL_API_STATES_ENTITY.format(entity_id))
else: else:
self._message( self._message(
"State of {} changed to {}".format(category, new_state)) "State of {} changed to {}".format(entity_id, new_state))
except KeyError: except KeyError:
# If new_state don't exist in post data # If new_state don't exist in post data
@ -607,20 +603,20 @@ class RequestHandler(BaseHTTPRequestHandler):
# pylint: disable=unused-argument # pylint: disable=unused-argument
def _handle_get_api_states(self, path_match, data): def _handle_get_api_states(self, path_match, data):
""" Returns the categories which state is being tracked. """ """ Returns the entitie ids which state are being tracked. """
self._write_json({'categories': self.server.statemachine.categories}) self._write_json({'entity_ids': self.server.statemachine.entity_ids})
# pylint: disable=unused-argument # pylint: disable=unused-argument
def _handle_get_api_states_category(self, path_match, data): def _handle_get_api_states_entity(self, path_match, data):
""" Returns the state of a specific category. """ """ Returns the state of a specific entity. """
category = path_match.group('category') entity_id = path_match.group('entity_id')
state = self.server.statemachine.get_state(category) state = self.server.statemachine.get_state(entity_id)
if state: if state:
self._write_json(state.to_json_dict(category)) self._write_json(state.to_json_dict(entity_id))
else: else:
# If category does not exist # If entity_id does not exist
self._message("State does not exist.", HTTP_UNPROCESSABLE_ENTITY) self._message("State does not exist.", HTTP_UNPROCESSABLE_ENTITY)
def _handle_get_api_events(self, path_match, data): def _handle_get_api_events(self, path_match, data):

View File

@ -14,21 +14,21 @@ import homeassistant.components.group as group
DOMAIN = "light" DOMAIN = "light"
STATE_GROUP_NAME_ALL_LIGHTS = 'all_lights' GROUP_NAME_ALL_LIGHTS = 'all_lights'
STATE_CATEGORY_ALL_LIGHTS = group.STATE_CATEGORY_FORMAT.format( ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format(
STATE_GROUP_NAME_ALL_LIGHTS) GROUP_NAME_ALL_LIGHTS)
STATE_CATEGORY_FORMAT = DOMAIN + ".{}" ENTITY_ID_FORMAT = DOMAIN + ".{}"
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
def is_on(statemachine, light_id=None): def is_on(statemachine, light_id=None):
""" Returns if the lights are on based on the statemachine. """ """ Returns if the lights are on based on the statemachine. """
category = STATE_CATEGORY_FORMAT.format(light_id) if light_id \ entity_id = ENTITY_ID_FORMAT.format(light_id) if light_id \
else STATE_CATEGORY_ALL_LIGHTS else ENTITY_ID_ALL_LIGHTS
return statemachine.is_state(category, ha.STATE_ON) return statemachine.is_state(entity_id, ha.STATE_ON)
def turn_on(bus, light_id=None, transition_seconds=None): def turn_on(bus, light_id=None, transition_seconds=None):
@ -80,21 +80,21 @@ def setup(bus, statemachine, light_control):
for light_id in light_control.light_ids} for light_id in light_control.light_ids}
for light_id, state in status.items(): for light_id, state in status.items():
state_category = STATE_CATEGORY_FORMAT.format(light_id) entity_id = ENTITY_ID_FORMAT.format(light_id)
new_state = ha.STATE_ON if state else ha.STATE_OFF new_state = ha.STATE_ON if state else ha.STATE_OFF
statemachine.set_state(state_category, new_state) statemachine.set_state(entity_id, new_state)
ha.track_time_change(bus, update_light_state, second=[0, 30]) ha.track_time_change(bus, update_light_state, second=[0, 30])
update_light_state(None) update_light_state(None)
# Track the all lights state # Track the all lights state
light_cats = [STATE_CATEGORY_FORMAT.format(light_id) for light_id entity_ids = [ENTITY_ID_FORMAT.format(light_id) for light_id
in light_control.light_ids] in light_control.light_ids]
group.setup(bus, statemachine, STATE_GROUP_NAME_ALL_LIGHTS, light_cats) group.setup(bus, statemachine, GROUP_NAME_ALL_LIGHTS, entity_ids)
def handle_light_service(service): def handle_light_service(service):
""" Hande a turn light on or off service call. """ """ Hande a turn light on or off service call. """

View File

@ -10,7 +10,7 @@ from datetime import timedelta
import homeassistant as ha import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
STATE_CATEGORY = "weather.sun" ENTITY_ID = "weather.sun"
STATE_ABOVE_HORIZON = "above_horizon" STATE_ABOVE_HORIZON = "above_horizon"
STATE_BELOW_HORIZON = "below_horizon" STATE_BELOW_HORIZON = "below_horizon"
@ -21,12 +21,12 @@ STATE_ATTR_NEXT_SETTING = "next_setting"
def is_up(statemachine): def is_up(statemachine):
""" Returns if the sun is currently up based on the statemachine. """ """ Returns if the sun is currently up based on the statemachine. """
return statemachine.is_state(STATE_CATEGORY, STATE_ABOVE_HORIZON) return statemachine.is_state(ENTITY_ID, STATE_ABOVE_HORIZON)
def next_setting(statemachine): def next_setting(statemachine):
""" Returns the datetime object representing the next sun setting. """ """ Returns the datetime object representing the next sun setting. """
state = statemachine.get_state(STATE_CATEGORY) state = statemachine.get_state(ENTITY_ID)
return None if not state else util.str_to_datetime( return None if not state else util.str_to_datetime(
state.attributes[STATE_ATTR_NEXT_SETTING]) state.attributes[STATE_ATTR_NEXT_SETTING])
@ -34,7 +34,7 @@ def next_setting(statemachine):
def next_rising(statemachine): def next_rising(statemachine):
""" Returns the datetime object representing the next sun setting. """ """ Returns the datetime object representing the next sun setting. """
state = statemachine.get_state(STATE_CATEGORY) state = statemachine.get_state(ENTITY_ID)
return None if not state else util.str_to_datetime( return None if not state else util.str_to_datetime(
state.attributes[STATE_ATTR_NEXT_RISING]) state.attributes[STATE_ATTR_NEXT_RISING])
@ -79,7 +79,7 @@ def setup(bus, statemachine, latitude, longitude):
STATE_ATTR_NEXT_SETTING: util.datetime_to_str(next_setting_dt) STATE_ATTR_NEXT_SETTING: util.datetime_to_str(next_setting_dt)
} }
statemachine.set_state(STATE_CATEGORY, new_state, state_attributes) statemachine.set_state(ENTITY_ID, new_state, state_attributes)
# +10 seconds to be sure that the change has occured # +10 seconds to be sure that the change has occured
ha.track_time_change(bus, update_sun_state, ha.track_time_change(bus, update_sun_state,

View File

@ -202,13 +202,13 @@ class StateMachine(ha.StateMachine):
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
@property @property
def categories(self): def entity_ids(self):
""" List of categories which states are being tracked. """ """ List of entity ids which states are being tracked. """
try: try:
req = self._call_api(METHOD_GET, hah.URL_API_STATES) req = self._call_api(METHOD_GET, hah.URL_API_STATES)
return req.json()['categories'] return req.json()['entity_ids']
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
self.logger.exception("StateMachine:Error connecting to server") self.logger.exception("StateMachine:Error connecting to server")
@ -218,19 +218,19 @@ class StateMachine(ha.StateMachine):
self.logger.exception("StateMachine:Got unexpected result") self.logger.exception("StateMachine:Got unexpected result")
return [] return []
except KeyError: # If 'categories' key not in parsed json except KeyError: # If 'entity_ids' key not in parsed json
self.logger.exception("StateMachine:Got unexpected result (2)") self.logger.exception("StateMachine:Got unexpected result (2)")
return [] return []
def remove_category(self, category): def remove_entity(self, entity_id):
""" This method is not implemented for remote statemachine. """ This method is not implemented for remote statemachine.
Throws NotImplementedError. """ Throws NotImplementedError. """
raise NotImplementedError raise NotImplementedError
def set_state(self, category, new_state, attributes=None): def set_state(self, entity_id, new_state, attributes=None):
""" Set the state of a category, add category if it does not exist. """ Set the state of a entity, add entity if it does not exist.
Attributes is an optional dict to specify attributes of this state. """ Attributes is an optional dict to specify attributes of this state. """
@ -243,7 +243,7 @@ class StateMachine(ha.StateMachine):
try: try:
req = self._call_api(METHOD_POST, req = self._call_api(METHOD_POST,
hah.URL_API_STATES_CATEGORY.format(category), hah.URL_API_STATES_ENTITY.format(entity_id),
data) data)
if req.status_code != 201: if req.status_code != 201:
@ -260,13 +260,12 @@ class StateMachine(ha.StateMachine):
finally: finally:
self.lock.release() self.lock.release()
def get_state(self, category): def get_state(self, entity_id):
""" Returns a dict (state,last_changed, attributes) describing """ Returns the state of the specified entity. """
the state of the specified category. """
try: try:
req = self._call_api(METHOD_GET, req = self._call_api(METHOD_GET,
hah.URL_API_STATES_CATEGORY.format(category)) hah.URL_API_STATES_ENTITY.format(entity_id))
if req.status_code == 200: if req.status_code == 200:
data = req.json() data = req.json()
@ -274,7 +273,7 @@ class StateMachine(ha.StateMachine):
return ha.State.from_json_dict(data) return ha.State.from_json_dict(data)
elif req.status_code == 422: elif req.status_code == 422:
# Category does not exist # Entity does not exist
return None return None
else: else:

View File

@ -77,12 +77,12 @@ class TestHTTPInterface(unittest.TestCase):
""" Test if we get access denied if we omit or provide """ Test if we get access denied if we omit or provide
a wrong api password. """ a wrong api password. """
req = requests.get( req = requests.get(
_url(hah.URL_API_STATES_CATEGORY.format("test"))) _url(hah.URL_API_STATES_ENTITY.format("test")))
self.assertEqual(req.status_code, 401) self.assertEqual(req.status_code, 401)
req = requests.get( req = requests.get(
_url(hah.URL_API_STATES_CATEGORY.format("test")), _url(hah.URL_API_STATES_ENTITY.format("test")),
params={"api_password": "not the password"}) params={"api_password": "not the password"})
self.assertEqual(req.status_code, 401) self.assertEqual(req.status_code, 401)
@ -92,7 +92,7 @@ class TestHTTPInterface(unittest.TestCase):
self.statemachine.set_state("test.test", "not_to_be_set_state") self.statemachine.set_state("test.test", "not_to_be_set_state")
requests.post(_url(hah.URL_CHANGE_STATE), requests.post(_url(hah.URL_CHANGE_STATE),
data={"category": "test.test", data={"entity_id": "test.test",
"new_state": "debug_state_change2", "new_state": "debug_state_change2",
"api_password": API_PASSWORD}) "api_password": API_PASSWORD})
@ -122,20 +122,20 @@ class TestHTTPInterface(unittest.TestCase):
self.assertEqual(len(test_value), 1) self.assertEqual(len(test_value), 1)
def test_api_list_state_categories(self): def test_api_list_state_entities(self):
""" Test if the debug interface allows us to list state categories. """ """ Test if the debug interface allows us to list state entities. """
req = requests.get(_url(hah.URL_API_STATES), req = requests.get(_url(hah.URL_API_STATES),
data={"api_password": API_PASSWORD}) data={"api_password": API_PASSWORD})
data = req.json() data = req.json()
self.assertEqual(self.statemachine.categories, self.assertEqual(self.statemachine.entity_ids,
data['categories']) data['entity_ids'])
def test_api_get_state(self): def test_api_get_state(self):
""" Test if the debug interface allows us to get a state. """ """ Test if the debug interface allows us to get a state. """
req = requests.get( req = requests.get(
_url(hah.URL_API_STATES_CATEGORY.format("test")), _url(hah.URL_API_STATES_ENTITY.format("test")),
data={"api_password": API_PASSWORD}) data={"api_password": API_PASSWORD})
data = ha.State.from_json_dict(req.json()) data = ha.State.from_json_dict(req.json())
@ -149,17 +149,17 @@ class TestHTTPInterface(unittest.TestCase):
def test_api_get_non_existing_state(self): def test_api_get_non_existing_state(self):
""" Test if the debug interface allows us to get a state. """ """ Test if the debug interface allows us to get a state. """
req = requests.get( req = requests.get(
_url(hah.URL_API_STATES_CATEGORY.format("does_not_exist")), _url(hah.URL_API_STATES_ENTITY.format("does_not_exist")),
params={"api_password": API_PASSWORD}) params={"api_password": API_PASSWORD})
self.assertEqual(req.status_code, 422) self.assertEqual(req.status_code, 422)
def test_api_state_change(self): def test_api_state_change(self):
""" Test if we can change the state of a category that exists. """ """ Test if we can change the state of an entity that exists. """
self.statemachine.set_state("test.test", "not_to_be_set_state") self.statemachine.set_state("test.test", "not_to_be_set_state")
requests.post(_url(hah.URL_API_STATES_CATEGORY.format("test.test")), requests.post(_url(hah.URL_API_STATES_ENTITY.format("test.test")),
data={"new_state": "debug_state_change2", data={"new_state": "debug_state_change2",
"api_password": API_PASSWORD}) "api_password": API_PASSWORD})
@ -167,20 +167,20 @@ class TestHTTPInterface(unittest.TestCase):
"debug_state_change2") "debug_state_change2")
# pylint: disable=invalid-name # pylint: disable=invalid-name
def test_api_state_change_of_non_existing_category(self): def test_api_state_change_of_non_existing_entity(self):
""" Test if the API allows us to change a state of """ Test if the API allows us to change a state of
a non existing category. """ a non existing entity. """
new_state = "debug_state_change" new_state = "debug_state_change"
req = requests.post( req = requests.post(
_url(hah.URL_API_STATES_CATEGORY.format( _url(hah.URL_API_STATES_ENTITY.format(
"test_category_that_does_not_exist")), "test_entity_that_does_not_exist")),
data={"new_state": new_state, data={"new_state": new_state,
"api_password": API_PASSWORD}) "api_password": API_PASSWORD})
cur_state = (self.statemachine. cur_state = (self.statemachine.
get_state("test_category_that_does_not_exist").state) get_state("test_entity_that_does_not_exist").state)
self.assertEqual(req.status_code, 201) self.assertEqual(req.status_code, 201)
self.assertEqual(cur_state, new_state) self.assertEqual(cur_state, new_state)
@ -326,14 +326,14 @@ class TestRemote(unittest.TestCase):
cls.sm_with_remote_eb.set_state("test", "a_state") cls.sm_with_remote_eb.set_state("test", "a_state")
# pylint: disable=invalid-name # pylint: disable=invalid-name
def test_remote_sm_list_state_categories(self): def test_remote_sm_list_state_entities(self):
""" Test if the debug interface allows us to list state categories. """ """ Test if the debug interface allows us to list state entity ids. """
self.assertEqual(self.statemachine.categories, self.assertEqual(self.statemachine.entity_ids,
self.remote_sm.categories) self.remote_sm.entity_ids)
def test_remote_sm_get_state(self): def test_remote_sm_get_state(self):
""" Test if the debug interface allows us to list state categories. """ """ Test if debug interface allows us to get state of an entity. """
remote_state = self.remote_sm.get_state("test") remote_state = self.remote_sm.get_state("test")
state = self.statemachine.get_state("test") state = self.statemachine.get_state("test")
@ -343,11 +343,11 @@ class TestRemote(unittest.TestCase):
self.assertEqual(remote_state.attributes, state.attributes) self.assertEqual(remote_state.attributes, state.attributes)
def test_remote_sm_get_non_existing_state(self): def test_remote_sm_get_non_existing_state(self):
""" Test if the debug interface allows us to list state categories. """ """ Test remote state machine to get state of non existing entity. """
self.assertEqual(self.remote_sm.get_state("test_does_not_exist"), None) self.assertEqual(self.remote_sm.get_state("test_does_not_exist"), None)
def test_remote_sm_state_change(self): def test_remote_sm_state_change(self):
""" Test if we can change the state of a category that exists. """ """ Test if we can change the state of an existing entity. """
self.remote_sm.set_state("test", "set_remotely", {"test": 1}) self.remote_sm.set_state("test", "set_remotely", {"test": 1})