mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 10:17:09 +00:00
RENAME: StateMachine tracks now entities, not categories.
This commit is contained in:
parent
e7f5953362
commit
e9e1b007ed
16
README.md
16
README.md
@ -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/<category>** - GET<br>
|
**/api/states/<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/<category>** - POST<br>
|
**/api/states/<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"
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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. """
|
||||||
|
@ -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,
|
||||||
|
@ -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:
|
||||||
|
@ -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})
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user