1
mirror of https://github.com/home-assistant/core synced 2024-07-12 07:21:24 +02:00

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:
**/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
{
"categories": [
"entity_ids": [
"Paulus_Nexus_4",
"weather.sun",
"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>
Returns the current state from a category
**/api/states/&lt;entity_id>** - GET<br>
Returns the current state from an entity
```json
{
@ -112,14 +112,14 @@ Returns the current state from a category
"next_rising": "07:04:15 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",
"state": "below_horizon"
}
```
**/api/states/&lt;category>** - 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>
**/api/states/&lt;entity_id>** - POST<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>
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_setting": "18:00:31 29-10-2013"
},
"category": "weather.sun",
"entity_id": "weather.sun",
"last_changed": "23:24:33 28-10-2013",
"state": "below_horizon"
}

View File

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

View File

@ -138,10 +138,10 @@ def from_config_file(config_path):
# Init 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),
group.setup(bus, statemachine, name,
categories.split(",")))
entity_ids.split(",")))
# Light trigger
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 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>".
Each component should publish services only under its own domain.

View File

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

View File

@ -22,20 +22,20 @@ def setup(bus, statemachine, light_group=None):
logger = logging.getLogger(__name__)
device_state_categories = ha.filter_categories(statemachine.categories,
device_tracker.DOMAIN)
device_entity_ids = ha.filter_entity_ids(statemachine.entity_ids,
device_tracker.DOMAIN)
if not device_state_categories:
if not device_entity_ids:
logger.error("LightTrigger:No devices found to track")
return False
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
light_ids = ha.filter_categories(
group.get_categories(statemachine, light_group), light.DOMAIN, True)
light_ids = ha.filter_entity_ids(
group.get_entity_ids(statemachine, light_group), light.DOMAIN, True)
if not light_ids:
logger.error("LightTrigger:No lights found to turn on ")
@ -48,7 +48,7 @@ def setup(bus, statemachine, light_group=None):
len(light_ids))
# 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.
We will schedule to have each light start after one another
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
# 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)
# If the sun is already above horizon
@ -84,14 +84,14 @@ def setup(bus, statemachine, light_group=None):
if sun.is_up(statemachine):
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. """
lights_are_on = group.is_on(statemachine, light_group)
light_needed = not (lights_are_on or sun.is_up(statemachine))
# 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):
# These variables are needed for the elif check
@ -103,7 +103,7 @@ def setup(bus, statemachine, light_group=None):
logger.info(
"Home coming event for {}. Turning lights on".
format(category))
format(entity))
for light_id in light_ids:
light.turn_on(bus, light_id)
@ -127,7 +127,7 @@ def setup(bus, statemachine, light_group=None):
break
# 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):
logger.info(
@ -136,12 +136,12 @@ def setup(bus, statemachine, light_group=None):
general.shutdown_devices(bus, statemachine)
# Track home coming of each seperate device
for category in device_state_categories:
ha.track_state_change(bus, category, handle_device_state_change,
for entity in device_entity_ids:
ha.track_state_change(bus, entity, handle_device_state_change,
ha.STATE_NOT_HOME, ha.STATE_HOME)
# 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,
ha.STATE_NOT_HOME)

View File

@ -24,11 +24,11 @@ DOMAIN = "device_tracker"
SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv"
STATE_GROUP_NAME_ALL_DEVICES = 'all_tracked_devices'
STATE_CATEGORY_ALL_DEVICES = group.STATE_CATEGORY_FORMAT.format(
STATE_GROUP_NAME_ALL_DEVICES)
GROUP_NAME_ALL_DEVICES = 'all_tracked_devices'
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format(
GROUP_NAME_ALL_DEVICES)
STATE_CATEGORY_FORMAT = DOMAIN + '.{}'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
# After how much time do we consider a device not home if
# it does not show up on scans
@ -43,10 +43,10 @@ KNOWN_DEVICES_FILE = "known_devices.csv"
def is_home(statemachine, device_id=None):
""" Returns if any or specified device is home. """
category = STATE_CATEGORY_FORMAT.format(device_id) if device_id \
else STATE_CATEGORY_ALL_DEVICES
entity = ENTITY_ID_FORMAT.format(device_id) if device_id \
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
@ -83,14 +83,14 @@ class DeviceTracker(object):
self.update_devices(device_scanner.scan_devices())
group.setup(bus, statemachine, STATE_GROUP_NAME_ALL_DEVICES,
list(self.device_state_categories))
group.setup(bus, statemachine, GROUP_NAME_ALL_DEVICES,
list(self.device_entity_ids))
@property
def device_state_categories(self):
""" Returns a set containing all categories
that are maintained for devices. """
return set([self.known_devices[device]['category'] for device
def device_entity_ids(self):
""" Returns a set containing all device entity ids
that are being tracked. """
return set([self.known_devices[device]['entity_id'] for device
in self.known_devices
if self.known_devices[device]['track']])
@ -111,7 +111,7 @@ class DeviceTracker(object):
self.known_devices[device]['last_seen'] = now
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
# 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.statemachine.set_state(
self.known_devices[device]['category'],
self.known_devices[device]['entity_id'],
ha.STATE_NOT_HOME)
# 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:
default_last_seen = datetime(1990, 1, 1)
# Temp variable to keep track of which categories we use
# so we can ensure we have unique categories.
used_categories = []
# Temp variable to keep track of which entity ids we use
# so we can ensure we have unique entity ids.
used_entity_ids = []
try:
for row in csv.DictReader(inp):
@ -195,23 +195,23 @@ class DeviceTracker(object):
row['last_seen'] = default_last_seen
# 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'] \
else "unnamed_device"
category = STATE_CATEGORY_FORMAT.format(name)
entity_id = ENTITY_ID_FORMAT.format(name)
tries = 1
while category in used_categories:
while entity_id in used_entity_ids:
tries += 1
suffix = "_{}".format(tries)
category = STATE_CATEGORY_FORMAT.format(
entity_id = ENTITY_ID_FORMAT.format(
name + suffix)
row['category'] = category
used_categories.append(category)
row['entity_id'] = entity_id
used_entity_ids.append(entity_id)
known_devices[device] = row
@ -220,21 +220,21 @@ class DeviceTracker(object):
"No devices to track. Please update {}.".format(
KNOWN_DEVICES_FILE))
# Remove categories that are no longer maintained
new_categories = set([known_devices[device]['category']
# Remove entities that are no longer maintained
new_entity_ids = set([known_devices[device]['entity_id']
for device in known_devices
if known_devices[device]['track']])
for category in \
self.device_state_categories - new_categories:
for entity_id in \
self.device_entity_ids - new_entity_ids:
self.logger.info(
"DeviceTracker:Removing category {}".format(
category))
self.statemachine.remove_category(category)
"DeviceTracker:Removing entity {}".format(
entity_id))
self.statemachine.remove_entity(entity_id)
# File parsed, warnings given if necessary
# categories cleaned up, make it available
# entities cleaned up, make it available
self.known_devices = known_devices
self.logger.info(

View File

@ -11,9 +11,9 @@ import homeassistant as ha
DOMAIN = "group"
STATE_CATEGORY_FORMAT = DOMAIN + ".{}"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
STATE_ATTR_CATEGORIES = "categories"
STATE_ATTR_ENTITY_IDS = "entity_ids"
_GROUP_TYPES = {
"on_off": (ha.STATE_ON, ha.STATE_OFF),
@ -46,29 +46,32 @@ def is_on(statemachine, group):
return False
def get_categories(statemachine, group):
""" Get the categories that make up this group. """
state = statemachine.get_state(group)
return state.attributes[STATE_ATTR_CATEGORIES] if state else []
def get_entity_ids(statemachine, group):
""" Get the entity ids that make up this group. """
try:
return statemachine.get_state(group).attributes[STATE_ATTR_ENTITY_IDS]
except (AttributeError, KeyError):
# AttributeError if state did not exist
# KeyError if key did not exist in attributes
return []
# 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
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
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)
# - if all states exist and have valid states
# - retrieve the current state of the group
errors = []
group_type, group_on, group_off, group_state = None, None, None, None
for cat in categories:
state = statemachine.get_state(cat)
for entity_id in entity_ids:
state = statemachine.get_state(entity_id)
# Try to determine group type if we didn't yet
if not group_type and state:
@ -85,15 +88,15 @@ def setup(bus, statemachine, name, categories):
break
# Check if category exists
# Check if entity exists
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:
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
elif group_state == group_off and state.state == group_on:
@ -105,15 +108,15 @@ def setup(bus, statemachine, name, categories):
return False
group_cat = STATE_CATEGORY_FORMAT.format(name)
state_attr = {STATE_ATTR_CATEGORIES: categories}
group_entity_id = ENTITY_ID_FORMAT.format(name)
state_attr = {STATE_ATTR_ENTITY_IDS: entity_ids}
# 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
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 = 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:
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:
# Check if any of the other states is still on
if not any([statemachine.is_state(cat, group_on)
for cat in categories if cat != category]):
statemachine.set_state(group_cat, group_off, state_attr)
if not any([statemachine.is_state(ent_id, group_on)
for ent_id in entity_ids if entity_id != ent_id]):
statemachine.set_state(group_entity_id, group_off, state_attr)
for cat in categories:
ha.track_state_change(bus, cat, _update_group_state)
for entity_id in entity_ids:
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

View File

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

View File

@ -14,21 +14,21 @@ import homeassistant.components.group as group
DOMAIN = "light"
STATE_GROUP_NAME_ALL_LIGHTS = 'all_lights'
STATE_CATEGORY_ALL_LIGHTS = group.STATE_CATEGORY_FORMAT.format(
STATE_GROUP_NAME_ALL_LIGHTS)
GROUP_NAME_ALL_LIGHTS = 'all_lights'
ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format(
GROUP_NAME_ALL_LIGHTS)
STATE_CATEGORY_FORMAT = DOMAIN + ".{}"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
def is_on(statemachine, light_id=None):
""" Returns if the lights are on based on the statemachine. """
category = STATE_CATEGORY_FORMAT.format(light_id) if light_id \
else STATE_CATEGORY_ALL_LIGHTS
entity_id = ENTITY_ID_FORMAT.format(light_id) if light_id \
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):
@ -80,21 +80,21 @@ def setup(bus, statemachine, light_control):
for light_id in light_control.light_ids}
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
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])
update_light_state(None)
# 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]
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):
""" 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.util as util
STATE_CATEGORY = "weather.sun"
ENTITY_ID = "weather.sun"
STATE_ABOVE_HORIZON = "above_horizon"
STATE_BELOW_HORIZON = "below_horizon"
@ -21,12 +21,12 @@ STATE_ATTR_NEXT_SETTING = "next_setting"
def is_up(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):
""" 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(
state.attributes[STATE_ATTR_NEXT_SETTING])
@ -34,7 +34,7 @@ def next_setting(statemachine):
def next_rising(statemachine):
""" 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(
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)
}
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
ha.track_time_change(bus, update_sun_state,

View File

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

View File

@ -77,12 +77,12 @@ class TestHTTPInterface(unittest.TestCase):
""" Test if we get access denied if we omit or provide
a wrong api password. """
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)
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"})
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")
requests.post(_url(hah.URL_CHANGE_STATE),
data={"category": "test.test",
data={"entity_id": "test.test",
"new_state": "debug_state_change2",
"api_password": API_PASSWORD})
@ -122,20 +122,20 @@ class TestHTTPInterface(unittest.TestCase):
self.assertEqual(len(test_value), 1)
def test_api_list_state_categories(self):
""" Test if the debug interface allows us to list state categories. """
def test_api_list_state_entities(self):
""" Test if the debug interface allows us to list state entities. """
req = requests.get(_url(hah.URL_API_STATES),
data={"api_password": API_PASSWORD})
data = req.json()
self.assertEqual(self.statemachine.categories,
data['categories'])
self.assertEqual(self.statemachine.entity_ids,
data['entity_ids'])
def test_api_get_state(self):
""" Test if the debug interface allows us to get a state. """
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 = ha.State.from_json_dict(req.json())
@ -149,17 +149,17 @@ class TestHTTPInterface(unittest.TestCase):
def test_api_get_non_existing_state(self):
""" Test if the debug interface allows us to get a state. """
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})
self.assertEqual(req.status_code, 422)
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")
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",
"api_password": API_PASSWORD})
@ -167,20 +167,20 @@ class TestHTTPInterface(unittest.TestCase):
"debug_state_change2")
# 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
a non existing category. """
a non existing entity. """
new_state = "debug_state_change"
req = requests.post(
_url(hah.URL_API_STATES_CATEGORY.format(
"test_category_that_does_not_exist")),
_url(hah.URL_API_STATES_ENTITY.format(
"test_entity_that_does_not_exist")),
data={"new_state": new_state,
"api_password": API_PASSWORD})
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(cur_state, new_state)
@ -326,14 +326,14 @@ class TestRemote(unittest.TestCase):
cls.sm_with_remote_eb.set_state("test", "a_state")
# pylint: disable=invalid-name
def test_remote_sm_list_state_categories(self):
""" Test if the debug interface allows us to list state categories. """
def test_remote_sm_list_state_entities(self):
""" Test if the debug interface allows us to list state entity ids. """
self.assertEqual(self.statemachine.categories,
self.remote_sm.categories)
self.assertEqual(self.statemachine.entity_ids,
self.remote_sm.entity_ids)
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")
state = self.statemachine.get_state("test")
@ -343,11 +343,11 @@ class TestRemote(unittest.TestCase):
self.assertEqual(remote_state.attributes, state.attributes)
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)
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})