Merge pull request #478 from balloob/dev

0.7.4rc1
This commit is contained in:
Paulus Schoutsen 2015-10-04 11:30:36 -07:00
commit 7f60f1e662
79 changed files with 3688 additions and 314 deletions

View File

@ -49,6 +49,7 @@ omit =
homeassistant/components/media_player/itunes.py homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/mpd.py homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/squeezebox.py homeassistant/components/media_player/squeezebox.py
homeassistant/components/media_player/sonos.py homeassistant/components/media_player/sonos.py
homeassistant/components/notify/file.py homeassistant/components/notify/file.py
@ -69,6 +70,7 @@ omit =
homeassistant/components/sensor/glances.py homeassistant/components/sensor/glances.py
homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/mysensors.py
homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/rest.py
homeassistant/components/sensor/rfxtrx.py homeassistant/components/sensor/rfxtrx.py
homeassistant/components/sensor/rpi_gpio.py homeassistant/components/sensor/rpi_gpio.py
homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/sabnzbd.py
@ -77,6 +79,7 @@ omit =
homeassistant/components/sensor/temper.py homeassistant/components/sensor/temper.py
homeassistant/components/sensor/time_date.py homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/transmission.py homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/worldclock.py
homeassistant/components/switch/arest.py homeassistant/components/switch/arest.py
homeassistant/components/switch/command_switch.py homeassistant/components/switch/command_switch.py
homeassistant/components/switch/edimax.py homeassistant/components/switch/edimax.py

View File

@ -1,5 +1,8 @@
sudo: false sudo: false
language: python language: python
cache:
directories:
- $HOME/virtualenv/python3.4.2/
python: python:
- "3.4" - "3.4"
install: install:

View File

@ -18,7 +18,7 @@ For help on building your component, please see the [developer documentation](ht
After you finish adding support for your device: After you finish adding support for your device:
- Update the supported devices in the `README.md` file. - Update the supported devices in the `README.md` file.
- Add any new dependencies to `requirements.txt`. - Add any new dependencies to `requirements_all.txt`. There is no ordering right now, so just add it to the end.
- Update the `.coveragerc` file. - Update the `.coveragerc` file.
- Provide some documentation for [home-assistant.io](https://home-assistant.io/). The documentation is handled in a separate [git repository](https://github.com/balloob/home-assistant.io). - Provide some documentation for [home-assistant.io](https://home-assistant.io/). The documentation is handled in a separate [git repository](https://github.com/balloob/home-assistant.io).
- Make sure all your code passes Pylint and flake8 (PEP8 and some more) validation. To generate reports, run `pylint homeassistant > pylint.txt` and `flake8 homeassistant --exclude bower_components,external > flake8.txt`. - Make sure all your code passes Pylint and flake8 (PEP8 and some more) validation. To generate reports, run `pylint homeassistant > pylint.txt` and `flake8 homeassistant --exclude bower_components,external > flake8.txt`.

View File

@ -1 +1,2 @@
recursive-exclude tests * recursive-exclude tests *
recursive-include homeassistant services.yaml

View File

@ -1,7 +1,9 @@
homeassistant: homeassistant:
# Omitted values in this section will be auto detected using freegeoip.net # Omitted values in this section will be auto detected using freegeoip.net
# Location required to calculate the time the sun rises and sets # Location required to calculate the time the sun rises and sets.
# Cooridinates are also used for location for weather related components.
# Google Maps can be used to determine more precise GPS cooridinates.
latitude: 32.87336 latitude: 32.87336
longitude: 117.22743 longitude: 117.22743
@ -68,11 +70,18 @@ device_sun_light_trigger:
# A comma separated list of states that have to be tracked as a single group # A comma separated list of states that have to be tracked as a single group
# Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME) # Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME)
# You can also have groups within groups.
group: group:
Home:
- group.living_room
- group.kitchen
living_room: living_room:
- light.Bowl - light.Bowl
- light.Ceiling - light.Ceiling
- light.TV_back_light - light.TV_back_light
kitchen:
- light.fan_bulb_1
- light.fan_bulb_2
children: children:
- device_tracker.child_1 - device_tracker.child_1
- device_tracker.child_2 - device_tracker.child_2
@ -94,28 +103,36 @@ browser:
keyboard: keyboard:
automation: automation:
platform: state - alias: 'Rule 1 Light on in the evening'
alias: Sun starts shining trigger:
- platform: sun
event: sunset
offset: "-01:00:00"
- platform: state
entity_id: group.all_devices
state: home
condition:
- platform: state
entity_id: group.all_devices
state: home
- platform: time
after: "16:00:00"
before: "23:00:00"
action:
service: homeassistant.turn_on
entity_id: group.living_room
state_entity_id: sun.sun - alias: 'Rule 2 - Away Mode'
# Next two are optional, omit to match all
state_from: below_horizon
state_to: above_horizon
execute_service: light.turn_off trigger:
service_entity_id: group.living_room - platform: state
entity_id: group.all_devices
state: 'not_home'
automation 2: condition: use_trigger_values
platform: time action:
alias: Beer o Clock service: light.turn_off
entity_id: group.all_lights
time_hours: 16
time_minutes: 0
time_seconds: 0
execute_service: notify.notify
service_data:
message: It's 4, time for beer!
sensor: sensor:
platform: systemmonitor platform: systemmonitor
@ -135,6 +152,23 @@ sensor:
- type: 'process' - type: 'process'
arg: 'octave-cli' arg: 'octave-cli'
sensor 2:
platform: forecast
api_key: <register on Forecast.io for your PRIVATE API>
monitored_conditions:
- summary
- precip_type
- precip_intensity
- temperature
- dew_point
- wind_speed
- wind_bearing
- cloud_cover
- humidity
- pressure
- visibility
- ozone
script: script:
# Turns on the bedroom lights and then the living room lights 1 minute later # Turns on the bedroom lights and then the living room lights 1 minute later
wakeup: wakeup:

View File

@ -186,8 +186,8 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
dict, {key: value or {} for key, value in config.items()}) dict, {key: value or {} for key, value in config.items()})
# Filter out the repeating and common config section [homeassistant] # Filter out the repeating and common config section [homeassistant]
components = (key for key in config.keys() components = set(key.split(' ')[0] for key in config.keys()
if ' ' not in key and key != core.DOMAIN) if key != core.DOMAIN)
if not core_components.setup(hass, config): if not core_components.setup(hass, config):
_LOGGER.error('Home Assistant core failed to initialize. ' _LOGGER.error('Home Assistant core failed to initialize. '
@ -297,11 +297,15 @@ def process_ha_core_config(hass, config):
else: else:
_LOGGER.error('Received invalid time zone %s', time_zone_str) _LOGGER.error('Received invalid time zone %s', time_zone_str)
for key, attr in ((CONF_LATITUDE, 'latitude'), for key, attr, typ in ((CONF_LATITUDE, 'latitude', float),
(CONF_LONGITUDE, 'longitude'), (CONF_LONGITUDE, 'longitude', float),
(CONF_NAME, 'location_name')): (CONF_NAME, 'location_name', str)):
if key in config: if key in config:
setattr(hac, attr, config[key]) try:
setattr(hac, attr, typ(config[key]))
except ValueError:
_LOGGER.error('Received invalid %s value for %s: %s',
typ.__name__, key, attr)
set_time_zone(config.get(CONF_TIME_ZONE)) set_time_zone(config.get(CONF_TIME_ZONE))

View File

@ -1,15 +1,18 @@
""" """
homeassistant.components.alarm_control_panel homeassistant.components.alarm_control_panel
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component to interface with a alarm control panel. Component to interface with a alarm control panel.
""" """
import logging import logging
from homeassistant.helpers.entity import Entity import os
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components import verisure from homeassistant.components import verisure
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY) SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
DOMAIN = 'alarm_control_panel' DOMAIN = 'alarm_control_panel'
DEPENDENCIES = [] DEPENDENCIES = []
@ -29,9 +32,11 @@ SERVICE_TO_METHOD = {
} }
ATTR_CODE = 'code' ATTR_CODE = 'code'
ATTR_CODE_FORMAT = 'code_format'
ATTR_TO_PROPERTY = [ ATTR_TO_PROPERTY = [
ATTR_CODE, ATTR_CODE,
ATTR_CODE_FORMAT
] ]
@ -57,8 +62,12 @@ def setup(hass, config):
for alarm in target_alarms: for alarm in target_alarms:
getattr(alarm, method)(code) getattr(alarm, method)(code)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
for service in SERVICE_TO_METHOD: for service in SERVICE_TO_METHOD:
hass.services.register(DOMAIN, service, alarm_service_handler) hass.services.register(DOMAIN, service, alarm_service_handler,
descriptions.get(service))
return True return True
@ -93,16 +102,31 @@ def alarm_arm_away(hass, code, entity_id=None):
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data)
# pylint: disable=no-self-use
class AlarmControlPanel(Entity): class AlarmControlPanel(Entity):
""" ABC for alarm control devices. """ """ ABC for alarm control devices. """
def alarm_disarm(self, code):
@property
def code_format(self):
""" regex for code format or None if no code is required. """
return None
def alarm_disarm(self, code=None):
""" Send disarm command. """ """ Send disarm command. """
raise NotImplementedError() raise NotImplementedError()
def alarm_arm_home(self, code): def alarm_arm_home(self, code=None):
""" Send arm home command. """ """ Send arm home command. """
raise NotImplementedError() raise NotImplementedError()
def alarm_arm_away(self, code): def alarm_arm_away(self, code=None):
""" Send arm away command. """ """ Send arm away command. """
raise NotImplementedError() raise NotImplementedError()
@property
def state_attributes(self):
""" Return the state attributes. """
state_attr = {
ATTR_CODE_FORMAT: self.code_format,
}
return state_attr

View File

@ -0,0 +1,167 @@
"""
homeassistant.components.alarm_control_panel.mqtt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This platform enables the possibility to control a MQTT alarm.
In this platform, 'state_topic' and 'command_topic' are required.
The alarm will only change state after receiving the a new state
from 'state_topic'. If these messages are published with RETAIN flag,
the MQTT alarm will receive an instant state update after subscription
and will start with correct state. Otherwise, the initial state will
be 'unknown'.
Configuration:
alarm_control_panel:
platform: mqtt
name: "MQTT Alarm"
state_topic: "home/alarm"
command_topic: "home/alarm/set"
qos: 0
payload_disarm: "DISARM"
payload_arm_home: "ARM_HOME"
payload_arm_away: "ARM_AWAY"
code: "mySecretCode"
Variables:
name
*Optional
The name of the alarm. Default is 'MQTT Alarm'.
state_topic
*Required
The MQTT topic subscribed to receive state updates.
command_topic
*Required
The MQTT topic to publish commands to change the alarm state.
qos
*Optional
The maximum QoS level of the state topic. Default is 0.
This QoS will also be used to publishing messages.
payload_disarm
*Optional
The payload do disarm alarm. Default is "DISARM".
payload_arm_home
*Optional
The payload to set armed-home mode. Default is "ARM_HOME".
payload_arm_away
*Optional
The payload to set armed-away mode. Default is "ARM_AWAY".
code
*Optional
If defined, specifies a code to enable or disable the alarm in the frontend.
"""
import logging
import homeassistant.components.mqtt as mqtt
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import (STATE_UNKNOWN)
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "MQTT Alarm"
DEFAULT_QOS = 0
DEFAULT_PAYLOAD_DISARM = "DISARM"
DEFAULT_PAYLOAD_ARM_HOME = "ARM_HOME"
DEFAULT_PAYLOAD_ARM_AWAY = "ARM_AWAY"
DEPENDENCIES = ['mqtt']
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the MQTT platform. """
if config.get('state_topic') is None:
_LOGGER.error("Missing required variable: state_topic")
return False
if config.get('command_topic') is None:
_LOGGER.error("Missing required variable: command_topic")
return False
add_devices([MqttAlarm(
hass,
config.get('name', DEFAULT_NAME),
config.get('state_topic'),
config.get('command_topic'),
config.get('qos', DEFAULT_QOS),
config.get('payload_disarm', DEFAULT_PAYLOAD_DISARM),
config.get('payload_arm_home', DEFAULT_PAYLOAD_ARM_HOME),
config.get('payload_arm_away', DEFAULT_PAYLOAD_ARM_AWAY),
config.get('code'))])
# pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttAlarm(alarm.AlarmControlPanel):
""" represents a MQTT alarm status within home assistant. """
def __init__(self, hass, name, state_topic, command_topic, qos,
payload_disarm, payload_arm_home, payload_arm_away, code):
self._state = STATE_UNKNOWN
self._hass = hass
self._name = name
self._state_topic = state_topic
self._command_topic = command_topic
self._qos = qos
self._payload_disarm = payload_disarm
self._payload_arm_home = payload_arm_home
self._payload_arm_away = payload_arm_away
self._code = code
def message_received(topic, payload, qos):
""" A new MQTT message has been received. """
self._state = payload
self.update_ha_state()
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
@property
def should_poll(self):
""" No polling needed """
return False
@property
def name(self):
""" Returns the name of the device. """
return self._name
@property
def state(self):
""" Returns the state of the device. """
return self._state
@property
def code_format(self):
""" One or more characters if code is defined """
return None if self._code is None else '.+'
def alarm_disarm(self, code=None):
""" Send disarm command. """
if code == str(self._code) or self.code_format is None:
mqtt.publish(self.hass, self._command_topic,
self._payload_disarm, self._qos)
else:
_LOGGER.warning("Wrong code entered while disarming!")
def alarm_arm_home(self, code=None):
""" Send arm home command. """
if code == str(self._code) or self.code_format is None:
mqtt.publish(self.hass, self._command_topic,
self._payload_arm_home, self._qos)
else:
_LOGGER.warning("Wrong code entered while arming home!")
def alarm_arm_away(self, code=None):
""" Send arm away command. """
if code == str(self._code) or self.code_format is None:
mqtt.publish(self.hass, self._command_topic,
self._payload_arm_away, self._qos)
else:
_LOGGER.warning("Wrong code entered while arming away!")

View File

@ -1,6 +1,6 @@
""" """
homeassistant.components.alarm_control_panel.verisure homeassistant.components.alarm_control_panel.verisure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Interfaces with Verisure alarm control panel. Interfaces with Verisure alarm control panel.
""" """
import logging import logging
@ -34,7 +34,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class VerisureAlarm(alarm.AlarmControlPanel): class VerisureAlarm(alarm.AlarmControlPanel):
""" represents a Verisure alarm status within home assistant. """ """ Represents a Verisure alarm status. """
def __init__(self, alarm_status): def __init__(self, alarm_status):
self._id = alarm_status.id self._id = alarm_status.id
@ -51,8 +51,13 @@ class VerisureAlarm(alarm.AlarmControlPanel):
""" Returns the state of the device. """ """ Returns the state of the device. """
return self._state return self._state
@property
def code_format(self):
""" Four digit code required. """
return '^\\d{4}$'
def update(self): def update(self):
''' update alarm status ''' """ Update alarm status """
verisure.update() verisure.update()
if verisure.STATUS[self._device][self._id].status == 'unarmed': if verisure.STATUS[self._device][self._id].status == 'unarmed':
@ -66,21 +71,21 @@ class VerisureAlarm(alarm.AlarmControlPanel):
'Unknown alarm state %s', 'Unknown alarm state %s',
verisure.STATUS[self._device][self._id].status) verisure.STATUS[self._device][self._id].status)
def alarm_disarm(self, code): def alarm_disarm(self, code=None):
""" Send disarm command. """ """ Send disarm command. """
verisure.MY_PAGES.set_alarm_status( verisure.MY_PAGES.set_alarm_status(
code, code,
verisure.MY_PAGES.ALARM_DISARMED) verisure.MY_PAGES.ALARM_DISARMED)
_LOGGER.warning('disarming') _LOGGER.warning('disarming')
def alarm_arm_home(self, code): def alarm_arm_home(self, code=None):
""" Send arm home command. """ """ Send arm home command. """
verisure.MY_PAGES.set_alarm_status( verisure.MY_PAGES.set_alarm_status(
code, code,
verisure.MY_PAGES.ALARM_ARMED_HOME) verisure.MY_PAGES.ALARM_ARMED_HOME)
_LOGGER.warning('arming home') _LOGGER.warning('arming home')
def alarm_arm_away(self, code): def alarm_arm_away(self, code=None):
""" Send arm away command. """ """ Send arm away command. """
verisure.MY_PAGES.set_alarm_status( verisure.MY_PAGES.set_alarm_status(
code, code,

View File

@ -103,6 +103,10 @@ def _handle_get_api_stream(handler, path_match, data):
write_lock = threading.Lock() write_lock = threading.Lock()
block = threading.Event() block = threading.Event()
restrict = data.get('restrict')
if restrict:
restrict = restrict.split(',')
def write_message(payload): def write_message(payload):
""" Writes a message to the output. """ """ Writes a message to the output. """
with write_lock: with write_lock:
@ -118,7 +122,8 @@ def _handle_get_api_stream(handler, path_match, data):
""" Forwards events to the open request. """ """ Forwards events to the open request. """
nonlocal gracefully_closed nonlocal gracefully_closed
if block.is_set() or event.event_type == EVENT_TIME_CHANGED: if block.is_set() or event.event_type == EVENT_TIME_CHANGED or \
restrict and event.event_type not in restrict:
return return
elif event.event_type == EVENT_HOMEASSISTANT_STOP: elif event.event_type == EVENT_HOMEASSISTANT_STOP:
gracefully_closed = True gracefully_closed = True

View File

@ -0,0 +1,85 @@
"""
homeassistant.components.automation.zone
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Offers zone automation rules.
"""
import logging
from homeassistant.components import zone
from homeassistant.helpers.event import track_state_change
from homeassistant.const import (
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL)
CONF_ENTITY_ID = "entity_id"
CONF_ZONE = "zone"
CONF_EVENT = "event"
EVENT_ENTER = "enter"
EVENT_LEAVE = "leave"
DEFAULT_EVENT = EVENT_ENTER
def trigger(hass, config, action):
""" Listen for state changes based on `config`. """
entity_id = config.get(CONF_ENTITY_ID)
zone_entity_id = config.get(CONF_ZONE)
if entity_id is None or zone_entity_id is None:
logging.getLogger(__name__).error(
"Missing trigger configuration key %s or %s", CONF_ENTITY_ID,
CONF_ZONE)
return False
event = config.get(CONF_EVENT, DEFAULT_EVENT)
def zone_automation_listener(entity, from_s, to_s):
""" Listens for state changes and calls action. """
if from_s and None in (from_s.attributes.get(ATTR_LATITUDE),
from_s.attributes.get(ATTR_LONGITUDE)) or \
None in (to_s.attributes.get(ATTR_LATITUDE),
to_s.attributes.get(ATTR_LONGITUDE)):
return
from_match = _in_zone(hass, zone_entity_id, from_s) if from_s else None
to_match = _in_zone(hass, zone_entity_id, to_s)
if event == EVENT_ENTER and not from_match and to_match or \
event == EVENT_LEAVE and from_match and not to_match:
action()
track_state_change(
hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL)
return True
def if_action(hass, config):
""" Wraps action method with zone based condition. """
entity_id = config.get(CONF_ENTITY_ID)
zone_entity_id = config.get(CONF_ZONE)
if entity_id is None or zone_entity_id is None:
logging.getLogger(__name__).error(
"Missing condition configuration key %s or %s", CONF_ENTITY_ID,
CONF_ZONE)
return False
def if_in_zone():
""" Test if condition. """
return _in_zone(hass, zone_entity_id, hass.states.get(entity_id))
return if_in_zone
def _in_zone(hass, zone_entity_id, state):
""" Check if state is in zone. """
if not state or None in (state.attributes.get(ATTR_LATITUDE),
state.attributes.get(ATTR_LONGITUDE)):
return False
zone_state = hass.states.get(zone_entity_id)
return zone_state and zone.in_zone(
zone_state, state.attributes.get(ATTR_LATITUDE),
state.attributes.get(ATTR_LONGITUDE),
state.attributes.get(ATTR_GPS_ACCURACY, 0))

View File

@ -33,10 +33,10 @@ def setup(hass, config):
# Setup sun # Setup sun
if not hass.config.latitude: if not hass.config.latitude:
hass.config.latitude = '32.87336' hass.config.latitude = 32.87336
if not hass.config.longitude: if not hass.config.longitude:
hass.config.longitude = '117.22743' hass.config.longitude = 117.22743
bootstrap.setup_component(hass, 'sun') bootstrap.setup_component(hass, 'sun')
@ -60,7 +60,7 @@ def setup(hass, config):
{'camera': { {'camera': {
'platform': 'generic', 'platform': 'generic',
'name': 'IP Camera', 'name': 'IP Camera',
'still_image_url': 'http://194.218.96.92/jpg/image.jpg', 'still_image_url': 'http://home-assistant.io/demo/webcam.jpg',
}}) }})
# Setup scripts # Setup scripts
@ -108,7 +108,9 @@ def setup(hass, config):
"http://graph.facebook.com/297400035/picture", "http://graph.facebook.com/297400035/picture",
ATTR_FRIENDLY_NAME: 'Paulus'}) ATTR_FRIENDLY_NAME: 'Paulus'})
hass.states.set("device_tracker.anne_therese", "not_home", hass.states.set("device_tracker.anne_therese", "not_home",
{ATTR_FRIENDLY_NAME: 'Anne Therese'}) {ATTR_FRIENDLY_NAME: 'Anne Therese',
'latitude': hass.config.latitude + 0.002,
'longitude': hass.config.longitude + 0.002})
hass.states.set("group.all_devices", "home", hass.states.set("group.all_devices", "home",
{ {

View File

@ -17,7 +17,12 @@ device_tracker:
# New found devices auto found # New found devices auto found
track_new_devices: yes track_new_devices: yes
# Maximum distance from home we consider people home
range_home: 100
""" """
# pylint: disable=too-many-instance-attributes, too-many-arguments
# pylint: disable=too-many-locals
import csv import csv
from datetime import timedelta from datetime import timedelta
import logging import logging
@ -25,7 +30,7 @@ import os
import threading import threading
from homeassistant.bootstrap import prepare_setup_platform from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.components import discovery, group from homeassistant.components import discovery, group, zone
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform from homeassistant.helpers import config_per_platform
@ -35,10 +40,11 @@ import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_PICTURE, DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME) ATTR_ENTITY_PICTURE, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
DOMAIN = "device_tracker" DOMAIN = "device_tracker"
DEPENDENCIES = [] DEPENDENCIES = ['zone']
GROUP_NAME_ALL_DEVICES = 'all devices' GROUP_NAME_ALL_DEVICES = 'all devices'
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices') ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
@ -52,7 +58,7 @@ CONF_TRACK_NEW = "track_new_devices"
DEFAULT_CONF_TRACK_NEW = True DEFAULT_CONF_TRACK_NEW = True
CONF_CONSIDER_HOME = 'consider_home' CONF_CONSIDER_HOME = 'consider_home'
DEFAULT_CONF_CONSIDER_HOME = 180 # seconds DEFAULT_CONSIDER_HOME = 180 # seconds
CONF_SCAN_INTERVAL = "interval_seconds" CONF_SCAN_INTERVAL = "interval_seconds"
DEFAULT_SCAN_INTERVAL = 12 DEFAULT_SCAN_INTERVAL = 12
@ -60,15 +66,17 @@ DEFAULT_SCAN_INTERVAL = 12
CONF_AWAY_HIDE = 'hide_if_away' CONF_AWAY_HIDE = 'hide_if_away'
DEFAULT_AWAY_HIDE = False DEFAULT_AWAY_HIDE = False
CONF_HOME_RANGE = 'home_range'
DEFAULT_HOME_RANGE = 100
SERVICE_SEE = 'see' SERVICE_SEE = 'see'
ATTR_LATITUDE = 'latitude'
ATTR_LONGITUDE = 'longitude'
ATTR_MAC = 'mac' ATTR_MAC = 'mac'
ATTR_DEV_ID = 'dev_id' ATTR_DEV_ID = 'dev_id'
ATTR_HOST_NAME = 'host_name' ATTR_HOST_NAME = 'host_name'
ATTR_LOCATION_NAME = 'location_name' ATTR_LOCATION_NAME = 'location_name'
ATTR_GPS = 'gps' ATTR_GPS = 'gps'
ATTR_BATTERY = 'battery'
DISCOVERY_PLATFORMS = { DISCOVERY_PLATFORMS = {
discovery.SERVICE_NETGEAR: 'netgear', discovery.SERVICE_NETGEAR: 'netgear',
@ -86,7 +94,7 @@ def is_on(hass, entity_id=None):
def see(hass, mac=None, dev_id=None, host_name=None, location_name=None, def see(hass, mac=None, dev_id=None, host_name=None, location_name=None,
gps=None): gps=None, gps_accuracy=None, battery=None):
""" Call service to notify you see device. """ """ Call service to notify you see device. """
data = {key: value for key, value in data = {key: value for key, value in
((ATTR_MAC, mac), ((ATTR_MAC, mac),
@ -106,13 +114,17 @@ def setup(hass, config):
os.remove(csv_path) os.remove(csv_path)
conf = config.get(DOMAIN, {}) conf = config.get(DOMAIN, {})
consider_home = util.convert(conf.get(CONF_CONSIDER_HOME), int, consider_home = timedelta(
DEFAULT_CONF_CONSIDER_HOME) seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int,
DEFAULT_CONSIDER_HOME))
track_new = util.convert(conf.get(CONF_TRACK_NEW), bool, track_new = util.convert(conf.get(CONF_TRACK_NEW), bool,
DEFAULT_CONF_TRACK_NEW) DEFAULT_CONF_TRACK_NEW)
home_range = util.convert(conf.get(CONF_HOME_RANGE), int,
DEFAULT_HOME_RANGE)
devices = load_config(yaml_path, hass, timedelta(seconds=consider_home)) devices = load_config(yaml_path, hass, consider_home, home_range)
tracker = DeviceTracker(hass, consider_home, track_new, devices) tracker = DeviceTracker(hass, consider_home, track_new, home_range,
devices)
def setup_platform(p_type, p_config, disc_info=None): def setup_platform(p_type, p_config, disc_info=None):
""" Setup a device tracker platform. """ """ Setup a device tracker platform. """
@ -158,22 +170,26 @@ def setup(hass, config):
""" Service to see a device. """ """ Service to see a device. """
args = {key: value for key, value in call.data.items() if key in args = {key: value for key, value in call.data.items() if key in
(ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME, (ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME,
ATTR_GPS)} ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY)}
tracker.see(**args) tracker.see(**args)
hass.services.register(DOMAIN, SERVICE_SEE, see_service) descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_SEE, see_service,
descriptions.get(SERVICE_SEE))
return True return True
class DeviceTracker(object): class DeviceTracker(object):
""" Track devices """ """ Track devices """
def __init__(self, hass, consider_home, track_new, devices): def __init__(self, hass, consider_home, track_new, home_range, devices):
self.hass = hass self.hass = hass
self.devices = {dev.dev_id: dev for dev in devices} self.devices = {dev.dev_id: dev for dev in devices}
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
self.consider_home = timedelta(seconds=consider_home) self.consider_home = consider_home
self.track_new = track_new self.track_new = track_new
self.home_range = home_range
self.lock = threading.Lock() self.lock = threading.Lock()
for device in devices: for device in devices:
@ -183,7 +199,7 @@ class DeviceTracker(object):
self.group = None self.group = None
def see(self, mac=None, dev_id=None, host_name=None, location_name=None, def see(self, mac=None, dev_id=None, host_name=None, location_name=None,
gps=None): gps=None, gps_accuracy=None, battery=None):
""" Notify device tracker that you see a device. """ """ Notify device tracker that you see a device. """
with self.lock: with self.lock:
if mac is None and dev_id is None: if mac is None and dev_id is None:
@ -198,20 +214,21 @@ class DeviceTracker(object):
device = self.devices.get(dev_id) device = self.devices.get(dev_id)
if device: if device:
device.seen(host_name, location_name, gps) device.seen(host_name, location_name, gps, gps_accuracy,
battery)
if device.track: if device.track:
device.update_ha_state() device.update_ha_state()
return return
# If no device can be found, create it # If no device can be found, create it
device = Device( device = Device(
self.hass, self.consider_home, self.track_new, dev_id, mac, self.hass, self.consider_home, self.home_range, self.track_new,
(host_name or dev_id).replace('_', ' ')) dev_id, mac, (host_name or dev_id).replace('_', ' '))
self.devices[dev_id] = device self.devices[dev_id] = device
if mac is not None: if mac is not None:
self.mac_to_dev[mac] = device self.mac_to_dev[mac] = device
device.seen(host_name, location_name, gps) device.seen(host_name, location_name, gps, gps_accuracy, battery)
if device.track: if device.track:
device.update_ha_state() device.update_ha_state()
@ -239,19 +256,20 @@ class DeviceTracker(object):
class Device(Entity): class Device(Entity):
""" Tracked device. """ """ Tracked device. """
# pylint: disable=too-many-instance-attributes, too-many-arguments
host_name = None host_name = None
location_name = None location_name = None
gps = None gps = None
gps_accuracy = 0
last_seen = None last_seen = None
battery = None
# Track if the last update of this device was HOME # Track if the last update of this device was HOME
last_update_home = False last_update_home = False
_state = STATE_NOT_HOME _state = STATE_NOT_HOME
def __init__(self, hass, consider_home, track, dev_id, mac, name=None, def __init__(self, hass, consider_home, home_range, track, dev_id, mac,
picture=None, away_hide=False): name=None, picture=None, away_hide=False):
self.hass = hass self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id) self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
@ -259,6 +277,8 @@ class Device(Entity):
# detected anymore. # detected anymore.
self.consider_home = consider_home self.consider_home = consider_home
# Distance in meters
self.home_range = home_range
# Device ID # Device ID
self.dev_id = dev_id self.dev_id = dev_id
self.mac = mac self.mac = mac
@ -273,6 +293,13 @@ class Device(Entity):
self.config_picture = picture self.config_picture = picture
self.away_hide = away_hide self.away_hide = away_hide
@property
def gps_home(self):
""" Return if device is within range of home. """
distance = max(
0, self.hass.config.distance(*self.gps) - self.gps_accuracy)
return self.gps is not None and distance <= self.home_range
@property @property
def name(self): def name(self):
""" Returns the name of the entity. """ """ Returns the name of the entity. """
@ -292,8 +319,12 @@ class Device(Entity):
attr[ATTR_ENTITY_PICTURE] = self.config_picture attr[ATTR_ENTITY_PICTURE] = self.config_picture
if self.gps: if self.gps:
attr[ATTR_LATITUDE] = self.gps[0], attr[ATTR_LATITUDE] = self.gps[0]
attr[ATTR_LONGITUDE] = self.gps[1], attr[ATTR_LONGITUDE] = self.gps[1]
attr[ATTR_GPS_ACCURACY] = self.gps_accuracy
if self.battery:
attr[ATTR_BATTERY] = self.battery
return attr return attr
@ -302,12 +333,23 @@ class Device(Entity):
""" If device should be hidden. """ """ If device should be hidden. """
return self.away_hide and self.state != STATE_HOME return self.away_hide and self.state != STATE_HOME
def seen(self, host_name=None, location_name=None, gps=None): def seen(self, host_name=None, location_name=None, gps=None,
gps_accuracy=0, battery=None):
""" Mark the device as seen. """ """ Mark the device as seen. """
self.last_seen = dt_util.utcnow() self.last_seen = dt_util.utcnow()
self.host_name = host_name self.host_name = host_name
self.location_name = location_name self.location_name = location_name
self.gps = gps self.gps_accuracy = gps_accuracy or 0
self.battery = battery
if gps is None:
self.gps = None
else:
try:
self.gps = tuple(float(val) for val in gps)
except ValueError:
_LOGGER.warning('Could not parse gps value for %s: %s',
self.dev_id, gps)
self.gps = None
self.update() self.update()
def stale(self, now=None): def stale(self, now=None):
@ -321,6 +363,16 @@ class Device(Entity):
return return
elif self.location_name: elif self.location_name:
self._state = self.location_name self._state = self.location_name
elif self.gps is not None:
zone_state = zone.active_zone(self.hass, self.gps[0], self.gps[1],
self.gps_accuracy)
if zone_state is None:
self._state = STATE_NOT_HOME
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
self._state = STATE_HOME
else:
self._state = zone_state.name
elif self.stale(): elif self.stale():
self._state = STATE_NOT_HOME self._state = STATE_NOT_HOME
self.last_update_home = False self.last_update_home = False
@ -338,18 +390,18 @@ def convert_csv_config(csv_path, yaml_path):
(util.slugify(row['name']) or DEVICE_DEFAULT_NAME).lower(), (util.slugify(row['name']) or DEVICE_DEFAULT_NAME).lower(),
used_ids) used_ids)
used_ids.add(dev_id) used_ids.add(dev_id)
device = Device(None, None, row['track'] == '1', dev_id, device = Device(None, None, None, row['track'] == '1', dev_id,
row['device'], row['name'], row['picture']) row['device'], row['name'], row['picture'])
update_config(yaml_path, dev_id, device) update_config(yaml_path, dev_id, device)
return True return True
def load_config(path, hass, consider_home): def load_config(path, hass, consider_home, home_range):
""" Load devices from YAML config file. """ """ Load devices from YAML config file. """
if not os.path.isfile(path): if not os.path.isfile(path):
return [] return []
return [ return [
Device(hass, consider_home, device.get('track', False), Device(hass, consider_home, home_range, device.get('track', False),
str(dev_id).lower(), str(device.get('mac')).upper(), str(dev_id).lower(), str(device.get('mac')).upper(),
device.get('name'), device.get('picture'), device.get('name'), device.get('picture'),
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE)) device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))

View File

@ -0,0 +1,50 @@
"""
homeassistant.components.device_tracker.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo platform for the device tracker.
device_tracker:
platform: demo
"""
import random
from homeassistant.components.device_tracker import DOMAIN
def setup_scanner(hass, config, see):
""" Set up a demo tracker. """
def offset():
""" Return random offset. """
return (random.randrange(500, 2000)) / 2e5 * random.choice((-1, 1))
def random_see(dev_id, name):
""" Randomize a sighting. """
see(
dev_id=dev_id,
host_name=name,
gps=(hass.config.latitude + offset(),
hass.config.longitude + offset()),
gps_accuracy=random.randrange(50, 150),
battery=random.randrange(10, 90)
)
def observe(call=None):
""" Observe three entities. """
random_see('demo_paulus', 'Paulus')
random_see('demo_anne_therese', 'Anne Therese')
observe()
see(
dev_id='demo_home_boy',
host_name='Home Boy',
gps=[hass.config.latitude - 0.00002, hass.config.longitude + 0.00002],
gps_accuracy=20,
battery=53
)
hass.services.register(DOMAIN, 'demo', observe)
return True

View File

@ -0,0 +1,54 @@
"""
homeassistant.components.device_tracker.owntracks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
OwnTracks platform for the device tracker.
device_tracker:
platform: owntracks
"""
import json
import logging
import homeassistant.components.mqtt as mqtt
DEPENDENCIES = ['mqtt']
LOCATION_TOPIC = 'owntracks/+/+'
def setup_scanner(hass, config, see):
""" Set up a OwnTracksks tracker. """
def owntracks_location_update(topic, payload, qos):
""" MQTT message received. """
# Docs on available data:
# http://owntracks.org/booklet/tech/json/#_typelocation
try:
data = json.loads(payload)
except ValueError:
# If invalid JSON
logging.getLogger(__name__).error(
'Unable to parse payload as JSON: %s', payload)
return
if not isinstance(data, dict) or data.get('_type') != 'location':
return
parts = topic.split('/')
kwargs = {
'dev_id': '{}_{}'.format(parts[1], parts[2]),
'host_name': parts[1],
'gps': (data['lat'], data['lon']),
}
if 'acc' in data:
kwargs['gps_accuracy'] = data['acc']
if 'batt' in data:
kwargs['battery'] = data['batt']
see(**kwargs)
mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1)
return True

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
DOMAIN = "discovery" DOMAIN = "discovery"
DEPENDENCIES = [] DEPENDENCIES = []
REQUIREMENTS = ['netdisco==0.4'] REQUIREMENTS = ['netdisco==0.4.2']
SCAN_INTERVAL = 300 # seconds SCAN_INTERVAL = 300 # seconds

View File

@ -21,7 +21,8 @@ _LOGGER = logging.getLogger(__name__)
FRONTEND_URLS = [ FRONTEND_URLS = [
URL_ROOT, '/logbook', '/history', '/devService', '/devState', '/devEvent'] URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
'/devEvent']
STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)') STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)')

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """ """ DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "5f35285bc502e3f69f564240fee04baa" VERSION = "c4722afa376379bc4457d54bb9a38cee"

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 68f6c6ae5d37a1f0fcd1c36a8803581f9367ac5f Subproject commit c8d99bc3ea21cdd7bfb39e7700f92ed09f4b9efd

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

View File

@ -12,7 +12,8 @@ from homeassistant.helpers.entity import Entity
import homeassistant.util as util import homeassistant.util as util
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, ATTR_ENTITY_ID, STATE_ON, STATE_OFF,
STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN) STATE_HOME, STATE_NOT_HOME, STATE_OPEN, STATE_CLOSED,
STATE_UNKNOWN)
DOMAIN = "group" DOMAIN = "group"
DEPENDENCIES = [] DEPENDENCIES = []
@ -22,7 +23,8 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
ATTR_AUTO = "auto" ATTR_AUTO = "auto"
# List of ON/OFF state tuples for groupable states # List of ON/OFF state tuples for groupable states
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)] _GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME),
(STATE_OPEN, STATE_CLOSED)]
def _get_group_on_off(state): def _get_group_on_off(state):

View File

@ -232,7 +232,12 @@ class RequestHandler(SimpleHTTPRequestHandler):
def log_message(self, fmt, *arguments): def log_message(self, fmt, *arguments):
""" Redirect built-in log to HA logging """ """ Redirect built-in log to HA logging """
_LOGGER.info(fmt, *arguments) if self.server.no_password_set:
_LOGGER.info(fmt, *arguments)
else:
_LOGGER.info(
fmt, *(arg.replace(self.server.api_password, '*******')
if isinstance(arg, str) else arg for arg in arguments))
def _handle_request(self, method): # pylint: disable=too-many-branches def _handle_request(self, method): # pylint: disable=too-many-branches
""" Does some common checks and calls appropriate method. """ """ Does some common checks and calls appropriate method. """

View File

@ -52,14 +52,14 @@ import logging
import os import os
import csv import csv
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.components import group, discovery, wink, isy994
from homeassistant.helpers.entity import ToggleEntity from homeassistant.config import load_yaml_config_file
import homeassistant.util as util
import homeassistant.util.color as color_util
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.components import group, discovery, wink, isy994 from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
import homeassistant.util as util
import homeassistant.util.color as color_util
DOMAIN = "light" DOMAIN = "light"
@ -275,11 +275,13 @@ def setup(hass, config):
light.update_ha_state(True) light.update_ha_state(True)
# Listen for light on and light off service calls # Listen for light on and light off service calls
hass.services.register(DOMAIN, SERVICE_TURN_ON, descriptions = load_yaml_config_file(
handle_light_service) os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_light_service,
descriptions.get(SERVICE_TURN_ON))
hass.services.register(DOMAIN, SERVICE_TURN_OFF, hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_light_service,
handle_light_service) descriptions.get(SERVICE_TURN_OFF))
return True return True

View File

@ -19,11 +19,15 @@ configuration.yaml file.
light: light:
platform: limitlessled platform: limitlessled
host: 192.168.1.10 bridges:
group_1_name: Living Room - host: 192.168.1.10
group_2_name: Bedroom group_1_name: Living Room
group_3_name: Office group_2_name: Bedroom
group_4_name: Kitchen group_3_name: Office
group_3_type: white
group_4_name: Kitchen
- host: 192.168.1.11
group_2_name: Basement
""" """
import logging import logging
@ -33,19 +37,30 @@ from homeassistant.components.light import (Light, ATTR_BRIGHTNESS,
from homeassistant.util.color import color_RGB_to_xy from homeassistant.util.color import color_RGB_to_xy
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['ledcontroller==1.0.7'] REQUIREMENTS = ['ledcontroller==1.1.0']
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Gets the LimitlessLED lights. """ """ Gets the LimitlessLED lights. """
import ledcontroller import ledcontroller
led = ledcontroller.LedController(config['host']) # Handle old configuration format:
bridges = config.get('bridges', [config])
for bridge_id, bridge in enumerate(bridges):
bridge['id'] = bridge_id
pool = ledcontroller.LedControllerPool([x['host'] for x in bridges])
lights = [] lights = []
for i in range(1, 5): for bridge in bridges:
if 'group_%d_name' % (i) in config: for i in range(1, 5):
lights.append(LimitlessLED(led, i, config['group_%d_name' % (i)])) name_key = 'group_%d_name' % i
if name_key in bridge:
group_type = bridge.get('group_%d_type' % i, 'rgbw')
lights.append(LimitlessLED.factory(pool, bridge['id'], i,
bridge[name_key],
group_type))
add_devices_callback(lights) add_devices_callback(lights)
@ -53,15 +68,57 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
class LimitlessLED(Light): class LimitlessLED(Light):
""" Represents a LimitlessLED light """ """ Represents a LimitlessLED light """
def __init__(self, led, group, name): @staticmethod
self.led = led def factory(pool, controller_id, group, name, group_type):
''' Construct a Limitless LED of the appropriate type '''
if group_type == 'white':
return WhiteLimitlessLED(pool, controller_id, group, name)
elif group_type == 'rgbw':
return RGBWLimitlessLED(pool, controller_id, group, name)
# pylint: disable=too-many-arguments
def __init__(self, pool, controller_id, group, name, group_type):
self.pool = pool
self.controller_id = controller_id
self.group = group self.group = group
self.pool.execute(self.controller_id, "set_group_type", self.group,
group_type)
# LimitlessLEDs don't report state, we have track it ourselves. # LimitlessLEDs don't report state, we have track it ourselves.
self.led.off(self.group) self.pool.execute(self.controller_id, "off", self.group)
self._name = name or DEVICE_DEFAULT_NAME self._name = name or DEVICE_DEFAULT_NAME
self._state = False self._state = False
@property
def should_poll(self):
""" No polling needed. """
return False
@property
def name(self):
""" Returns the name of the device if any. """
return self._name
@property
def is_on(self):
""" True if device is on. """
return self._state
def turn_off(self, **kwargs):
""" Turn the device off. """
self._state = False
self.pool.execute(self.controller_id, "off", self.group)
self.update_ha_state()
class RGBWLimitlessLED(LimitlessLED):
""" Represents a RGBW LimitlessLED light """
def __init__(self, pool, controller_id, group, name):
super().__init__(pool, controller_id, group, name, 'rgbw')
self._brightness = 100 self._brightness = 100
self._xy_color = color_RGB_to_xy(255, 255, 255) self._xy_color = color_RGB_to_xy(255, 255, 255)
@ -87,16 +144,6 @@ class LimitlessLED(Light):
((0xE6, 0xE6, 0xFA), 'lavendar'), ((0xE6, 0xE6, 0xFA), 'lavendar'),
]] ]]
@property
def should_poll(self):
""" No polling needed for a demo light. """
return False
@property
def name(self):
""" Returns the name of the device if any. """
return self._name
@property @property
def brightness(self): def brightness(self):
return self._brightness return self._brightness
@ -117,11 +164,6 @@ class LimitlessLED(Light):
# First candidate in the sorted list is closest to desired color: # First candidate in the sorted list is closest to desired color:
return sorted(candidates)[0][1] return sorted(candidates)[0][1]
@property
def is_on(self):
""" True if device is on. """
return self._state
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
""" Turn the device on. """ """ Turn the device on. """
self._state = True self._state = True
@ -132,12 +174,21 @@ class LimitlessLED(Light):
if ATTR_XY_COLOR in kwargs: if ATTR_XY_COLOR in kwargs:
self._xy_color = kwargs[ATTR_XY_COLOR] self._xy_color = kwargs[ATTR_XY_COLOR]
self.led.set_color(self._xy_to_led_color(self._xy_color), self.group) self.pool.execute(self.controller_id, "set_color",
self.led.set_brightness(self._brightness / 255.0, self.group) self._xy_to_led_color(self._xy_color), self.group)
self.pool.execute(self.controller_id, "set_brightness",
self._brightness / 255.0, self.group)
self.update_ha_state() self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """ class WhiteLimitlessLED(LimitlessLED):
self._state = False """ Represents a White LimitlessLED light """
self.led.off(self.group)
def __init__(self, pool, controller_id, group, name):
super().__init__(pool, controller_id, group, name, 'white')
def turn_on(self, **kwargs):
""" Turn the device on. """
self._state = True
self.pool.execute(self.controller_id, "on", self.group)
self.update_ha_state() self.update_ha_state()

View File

@ -0,0 +1,52 @@
# Describes the format for available light services
turn_on:
description: Turn a light on
fields:
entity_id:
description: Name(s) of entities to turn on
example: 'light.kitchen'
transition:
description: Duration in seconds it takes to get to next state
example: 60
rgb_color:
description: Color for the light in RGB-format
example: '[255, 100, 100]'
xy_color:
description: Color for the light in XY-format
example: '[0.52, 0.43]'
brightness:
description: Number between 0..255 indicating brightness
example: 120
profile:
description: Name of a light profile to use
example: relax
flash:
description: If the light should flash
values:
- short
- long
effect:
description: Light effect
values:
- colorloop
turn_off:
description: Turn a light off
fields:
entity_id:
description: Name(s) of entities to turn off
example: 'light.kitchen'
transition:
description: Duration in seconds it takes to get to next state
example: 60

View File

@ -6,12 +6,14 @@ Support for Tellstick lights.
import logging import logging
# pylint: disable=no-name-in-module, import-error # pylint: disable=no-name-in-module, import-error
from homeassistant.components.light import Light, ATTR_BRIGHTNESS from homeassistant.components.light import Light, ATTR_BRIGHTNESS
from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
ATTR_FRIENDLY_NAME)
import tellcore.constants as tellcore_constants import tellcore.constants as tellcore_constants
from tellcore.library import DirectCallbackDispatcher
REQUIREMENTS = ['tellcore-py==1.0.4'] REQUIREMENTS = ['tellcore-py==1.1.2']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return Tellstick lights. """ """ Find and return Tellstick lights. """
@ -22,13 +24,32 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"Failed to import tellcore") "Failed to import tellcore")
return [] return []
core = telldus.TelldusCore() core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher())
switches_and_lights = core.devices() switches_and_lights = core.devices()
lights = [] lights = []
for switch in switches_and_lights: for switch in switches_and_lights:
if switch.methods(tellcore_constants.TELLSTICK_DIM): if switch.methods(tellcore_constants.TELLSTICK_DIM):
lights.append(TellstickLight(switch)) lights.append(TellstickLight(switch))
def _device_event_callback(id_, method, data, cid):
""" Called from the TelldusCore library to update one device """
for light_device in lights:
if light_device.tellstick_device.id == id_:
# Execute the update in another thread
light_device.update_ha_state(True)
break
callback_id = core.register_device_event(_device_event_callback)
def unload_telldus_lib(event):
""" Un-register the callback bindings """
if callback_id is not None:
core.unregister_callback(callback_id)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, unload_telldus_lib)
add_devices_callback(lights) add_devices_callback(lights)
@ -40,15 +61,15 @@ class TellstickLight(Light):
tellcore_constants.TELLSTICK_UP | tellcore_constants.TELLSTICK_UP |
tellcore_constants.TELLSTICK_DOWN) tellcore_constants.TELLSTICK_DOWN)
def __init__(self, tellstick): def __init__(self, tellstick_device):
self.tellstick = tellstick self.tellstick_device = tellstick_device
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name} self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
self._brightness = 0 self._brightness = 0
@property @property
def name(self): def name(self):
""" Returns the name of the switch if any. """ """ Returns the name of the switch if any. """
return self.tellstick.name return self.tellstick_device.name
@property @property
def is_on(self): def is_on(self):
@ -62,8 +83,9 @@ class TellstickLight(Light):
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
""" Turns the switch off. """ """ Turns the switch off. """
self.tellstick.turn_off() self.tellstick_device.turn_off()
self._brightness = 0 self._brightness = 0
self.update_ha_state()
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
""" Turns the switch on. """ """ Turns the switch on. """
@ -74,11 +96,12 @@ class TellstickLight(Light):
else: else:
self._brightness = brightness self._brightness = brightness
self.tellstick.dim(self._brightness) self.tellstick_device.dim(self._brightness)
self.update_ha_state()
def update(self): def update(self):
""" Update state of the light. """ """ Update state of the light. """
last_command = self.tellstick.last_sent_command( last_command = self.tellstick_device.last_sent_command(
self.last_sent_command_mask) self.last_sent_command_mask)
if last_command == tellcore_constants.TELLSTICK_TURNON: if last_command == tellcore_constants.TELLSTICK_TURNON:
@ -88,6 +111,11 @@ class TellstickLight(Light):
elif (last_command == tellcore_constants.TELLSTICK_DIM or elif (last_command == tellcore_constants.TELLSTICK_DIM or
last_command == tellcore_constants.TELLSTICK_UP or last_command == tellcore_constants.TELLSTICK_UP or
last_command == tellcore_constants.TELLSTICK_DOWN): last_command == tellcore_constants.TELLSTICK_DOWN):
last_sent_value = self.tellstick.last_sent_value() last_sent_value = self.tellstick_device.last_sent_value()
if last_sent_value is not None: if last_sent_value is not None:
self._brightness = last_sent_value self._brightness = last_sent_value
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False

View File

@ -10,7 +10,7 @@ import re
from homeassistant.core import State, DOMAIN as HA_DOMAIN from homeassistant.core import State, DOMAIN as HA_DOMAIN
from homeassistant.const import ( from homeassistant.const import (
EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF, EVENT_STATE_CHANGED, STATE_NOT_HOME, STATE_ON, STATE_OFF,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST) EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST)
from homeassistant import util from homeassistant import util
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -162,10 +162,12 @@ def humanify(events):
to_state = State.from_dict(event.data.get('new_state')) to_state = State.from_dict(event.data.get('new_state'))
# if last_changed == last_updated only attributes have changed # if last_changed != last_updated only attributes have changed
# we do not report on that yet. # we do not report on that yet. Also filter auto groups.
if not to_state or \ if not to_state or \
to_state.last_changed != to_state.last_updated: to_state.last_changed != to_state.last_updated or \
to_state.domain == 'group' and \
to_state.attributes.get('auto', False):
continue continue
domain = to_state.domain domain = to_state.domain
@ -218,10 +220,13 @@ def humanify(events):
def _entry_message_from_state(domain, state): def _entry_message_from_state(domain, state):
""" Convert a state to a message for the logbook. """ """ Convert a state to a message for the logbook. """
# We pass domain in so we don't have to split entity_id again # We pass domain in so we don't have to split entity_id again
# pylint: disable=too-many-return-statements
if domain == 'device_tracker': if domain == 'device_tracker':
return '{} home'.format( if state.state == STATE_NOT_HOME:
'arrived' if state.state == STATE_HOME else 'left') return 'is away'
else:
return 'is at {}'.format(state.state)
elif domain == 'sun': elif domain == 'sun':
if state.state == sun.STATE_ABOVE_HORIZON: if state.state == sun.STATE_ABOVE_HORIZON:

View File

@ -5,8 +5,10 @@ homeassistant.components.media_player
Component to interface with various media players. Component to interface with various media players.
""" """
import logging import logging
import os
from homeassistant.components import discovery from homeassistant.components import discovery
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import ( from homeassistant.const import (
@ -186,6 +188,9 @@ def setup(hass, config):
component.setup(config) component.setup(config)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def media_player_service_handler(service): def media_player_service_handler(service):
""" Maps services to methods on MediaPlayerDevice. """ """ Maps services to methods on MediaPlayerDevice. """
target_players = component.extract_from_service(service) target_players = component.extract_from_service(service)
@ -199,7 +204,8 @@ def setup(hass, config):
player.update_ha_state(True) player.update_ha_state(True)
for service in SERVICE_TO_METHOD: for service in SERVICE_TO_METHOD:
hass.services.register(DOMAIN, service, media_player_service_handler) hass.services.register(DOMAIN, service, media_player_service_handler,
descriptions.get(service))
def volume_set_service(service): def volume_set_service(service):
""" Set specified volume on the media player. """ """ Set specified volume on the media player. """
@ -216,7 +222,8 @@ def setup(hass, config):
if player.should_poll: if player.should_poll:
player.update_ha_state(True) player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service) hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service,
descriptions.get(SERVICE_VOLUME_SET))
def volume_mute_service(service): def volume_mute_service(service):
""" Mute (true) or unmute (false) the media player. """ """ Mute (true) or unmute (false) the media player. """
@ -233,7 +240,8 @@ def setup(hass, config):
if player.should_poll: if player.should_poll:
player.update_ha_state(True) player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service) hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service,
descriptions.get(SERVICE_VOLUME_MUTE))
def media_seek_service(service): def media_seek_service(service):
""" Seek to a position. """ """ Seek to a position. """
@ -250,7 +258,8 @@ def setup(hass, config):
if player.should_poll: if player.should_poll:
player.update_ha_state(True) player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service) hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service,
descriptions.get(SERVICE_MEDIA_SEEK))
def play_youtube_video_service(service, media_id=None): def play_youtube_video_service(service, media_id=None):
""" Plays specified media_id on the media player. """ """ Plays specified media_id on the media player. """
@ -268,14 +277,17 @@ def setup(hass, config):
hass.services.register( hass.services.register(
DOMAIN, "start_fireplace", DOMAIN, "start_fireplace",
lambda service: play_youtube_video_service(service, "eyU3bRy2x44")) lambda service: play_youtube_video_service(service, "eyU3bRy2x44"),
descriptions.get('start_fireplace'))
hass.services.register( hass.services.register(
DOMAIN, "start_epic_sax", DOMAIN, "start_epic_sax",
lambda service: play_youtube_video_service(service, "kxopViU98Xo")) lambda service: play_youtube_video_service(service, "kxopViU98Xo"),
descriptions.get('start_epic_sax'))
hass.services.register( hass.services.register(
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service) DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service,
descriptions.get(SERVICE_YOUTUBE_VIDEO))
return True return True

View File

@ -0,0 +1,237 @@
"""
homeassistant.components.media_player.plex
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides an interface to the Plex API
Configuration:
To use Plex add something like this to your configuration:
media_player:
platform: plex
name: plex_server
user: plex
password: my_secure_password
Variables:
name
*Required
The name of the backend device (Under Plex Media Server > settings > server).
user
*Required
The Plex username
password
*Required
The Plex password
"""
import logging
from datetime import timedelta
from homeassistant.components.media_player import (
MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
SUPPORT_NEXT_TRACK, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
from homeassistant.const import (
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_UNKNOWN)
import homeassistant.util as util
REQUIREMENTS = ['https://github.com/adrienbrault/python-plexapi/archive/'
'df2d0847e801d6d5cda920326d693cf75f304f1a.zip'
'#python-plexapi==1.0.2']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
_LOGGER = logging.getLogger(__name__)
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
# pylint: disable=abstract-method
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the plex platform. """
from plexapi.myplex import MyPlexUser
from plexapi.exceptions import BadRequest
name = config.get('name', '')
user = config.get('user', '')
password = config.get('password', '')
plexuser = MyPlexUser.signin(user, password)
plexserver = plexuser.getResource(name).connect()
plex_clients = {}
plex_sessions = {}
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_devices():
""" Updates the devices objects """
try:
devices = plexuser.devices()
except BadRequest:
_LOGGER.exception("Error listing plex devices")
return
new_plex_clients = []
for device in devices:
if (all(x not in ['client', 'player'] for x in device.provides)
or 'PlexAPI' == device.product):
continue
if device.clientIdentifier not in plex_clients:
new_client = PlexClient(device, plex_sessions, update_devices,
update_sessions)
plex_clients[device.clientIdentifier] = new_client
new_plex_clients.append(new_client)
else:
plex_clients[device.clientIdentifier].set_device(device)
if new_plex_clients:
add_devices(new_plex_clients)
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_sessions():
""" Updates the sessions objects """
try:
sessions = plexserver.sessions()
except BadRequest:
_LOGGER.exception("Error listing plex sessions")
return
plex_sessions.clear()
for session in sessions:
plex_sessions[session.player.machineIdentifier] = session
update_devices()
update_sessions()
class PlexClient(MediaPlayerDevice):
""" Represents a Plex device. """
# pylint: disable=too-many-public-methods
def __init__(self, device, plex_sessions, update_devices, update_sessions):
self.plex_sessions = plex_sessions
self.update_devices = update_devices
self.update_sessions = update_sessions
self.set_device(device)
def set_device(self, device):
""" Sets the device property """
self.device = device
@property
def session(self):
""" Returns the session, if any """
if self.device.clientIdentifier not in self.plex_sessions:
return None
return self.plex_sessions[self.device.clientIdentifier]
@property
def name(self):
""" Returns the name of the device. """
return self.device.name or self.device.product or self.device.device
@property
def state(self):
""" Returns the state of the device. """
if self.session:
state = self.session.player.state
if state == 'playing':
return STATE_PLAYING
elif state == 'paused':
return STATE_PAUSED
elif self.device.isReachable:
return STATE_IDLE
else:
return STATE_OFF
return STATE_UNKNOWN
def update(self):
self.update_devices(no_throttle=True)
self.update_sessions(no_throttle=True)
@property
def media_content_id(self):
""" Content ID of current playing media. """
if self.session is not None:
return self.session.ratingKey
@property
def media_content_type(self):
""" Content type of current playing media. """
if self.session is None:
return None
media_type = self.session.type
if media_type == 'episode':
return MEDIA_TYPE_TVSHOW
elif media_type == 'movie':
return MEDIA_TYPE_VIDEO
return None
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
if self.session is not None:
return self.session.duration
@property
def media_image_url(self):
""" Image url of current playing media. """
if self.session is not None:
return self.session.thumbUrl
@property
def media_title(self):
""" Title of current playing media. """
# find a string we can use as a title
if self.session is not None:
return self.session.title
@property
def media_season(self):
""" Season of curent playing media. (TV Show only) """
from plexapi.video import Show
if isinstance(self.session, Show):
return self.session.seasons()[0].index
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
from plexapi.video import Show
if isinstance(self.session, Show):
return self.session.grandparentTitle
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
from plexapi.video import Show
if isinstance(self.session, Show):
return self.session.index
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return SUPPORT_PLEX
def media_play(self):
""" media_play media player. """
self.device.play({'type': 'video'})
def media_pause(self):
""" media_pause media player. """
self.device.pause({'type': 'video'})
def media_next_track(self):
""" Send next track command. """
self.device.skipNext({'type': 'video'})
def media_previous_track(self):
""" Send previous track command. """
self.device.skipPrevious({'type': 'video'})

View File

@ -23,6 +23,7 @@ mqtt:
keepalive: 60 keepalive: 60
username: your_username username: your_username
password: your_secret_password password: your_secret_password
certificate: /home/paulus/dev/addtrustexternalcaroot.crt
Variables: Variables:
@ -42,8 +43,13 @@ Default is a random generated one.
keepalive keepalive
*Optional *Optional
The keep alive in seconds for this client. Default is 60. The keep alive in seconds for this client. Default is 60.
certificate
*Optional
Certificate to use for encrypting the connection to the broker.
""" """
import logging import logging
import os
import socket import socket
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -74,6 +80,7 @@ CONF_CLIENT_ID = 'client_id'
CONF_KEEPALIVE = 'keepalive' CONF_KEEPALIVE = 'keepalive'
CONF_USERNAME = 'username' CONF_USERNAME = 'username'
CONF_PASSWORD = 'password' CONF_PASSWORD = 'password'
CONF_CERTIFICATE = 'certificate'
ATTR_TOPIC = 'topic' ATTR_TOPIC = 'topic'
ATTR_PAYLOAD = 'payload' ATTR_PAYLOAD = 'payload'
@ -119,11 +126,18 @@ def setup(hass, config):
keepalive = util.convert(conf.get(CONF_KEEPALIVE), int, DEFAULT_KEEPALIVE) keepalive = util.convert(conf.get(CONF_KEEPALIVE), int, DEFAULT_KEEPALIVE)
username = util.convert(conf.get(CONF_USERNAME), str) username = util.convert(conf.get(CONF_USERNAME), str)
password = util.convert(conf.get(CONF_PASSWORD), str) password = util.convert(conf.get(CONF_PASSWORD), str)
certificate = util.convert(conf.get(CONF_CERTIFICATE), str)
# For cloudmqtt.com, secured connection, auto fill in certificate
if certificate is None and 19999 < port < 30000 and \
broker.endswith('.cloudmqtt.com'):
certificate = os.path.join(os.path.dirname(__file__),
'addtrustexternalcaroot.crt')
global MQTT_CLIENT global MQTT_CLIENT
try: try:
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username, MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username,
password) password, certificate)
except socket.error: except socket.error:
_LOGGER.exception("Can't connect to the broker. " _LOGGER.exception("Can't connect to the broker. "
"Please check your settings and the broker " "Please check your settings and the broker "
@ -161,7 +175,7 @@ def setup(hass, config):
class MQTT(object): # pragma: no cover class MQTT(object): # pragma: no cover
""" Implements messaging service for MQTT. """ """ Implements messaging service for MQTT. """
def __init__(self, hass, broker, port, client_id, keepalive, username, def __init__(self, hass, broker, port, client_id, keepalive, username,
password): password, certificate):
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
self.hass = hass self.hass = hass
@ -172,8 +186,12 @@ class MQTT(object): # pragma: no cover
self._mqttc = mqtt.Client() self._mqttc = mqtt.Client()
else: else:
self._mqttc = mqtt.Client(client_id) self._mqttc = mqtt.Client(client_id)
if username is not None: if username is not None:
self._mqttc.username_pw_set(username, password) self._mqttc.username_pw_set(username, password)
if certificate is not None:
self._mqttc.tls_set(certificate)
self._mqttc.on_subscribe = self._mqtt_on_subscribe self._mqttc.on_subscribe = self._mqtt_on_subscribe
self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe
self._mqttc.on_connect = self._mqtt_on_connect self._mqttc.on_connect = self._mqtt_on_connect
@ -209,6 +227,17 @@ class MQTT(object): # pragma: no cover
def _mqtt_on_connect(self, mqttc, obj, flags, result_code): def _mqtt_on_connect(self, mqttc, obj, flags, result_code):
""" On connect, resubscribe to all topics we were subscribed to. """ """ On connect, resubscribe to all topics we were subscribed to. """
if result_code != 0:
_LOGGER.error('Unable to connect to the MQTT broker: %s', {
1: 'Incorrect protocol version',
2: 'Invalid client identifier',
3: 'Server unavailable',
4: 'Bad username or password',
5: 'Not authorised'
}.get(result_code))
self._mqttc.disconnect()
return
old_topics = self.topics old_topics = self.topics
self._progress = {} self._progress = {}
self.topics = {} self.topics = {}

View File

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU
MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs
IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290
MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux
FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h
bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v
dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt
H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9
uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX
mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX
a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN
E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0
WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD
VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0
Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU
cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx
IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN
AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH
YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC
Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
-----END CERTIFICATE-----

View File

@ -6,7 +6,9 @@ Provides functionality to notify people.
""" """
from functools import partial from functools import partial
import logging import logging
import os
from homeassistant.config import load_yaml_config_file
from homeassistant.loader import get_component from homeassistant.loader import get_component
from homeassistant.helpers import config_per_platform from homeassistant.helpers import config_per_platform
@ -36,6 +38,9 @@ def setup(hass, config):
""" Sets up notify services. """ """ Sets up notify services. """
success = False success = False
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
for platform, p_config in config_per_platform(config, DOMAIN, _LOGGER): for platform, p_config in config_per_platform(config, DOMAIN, _LOGGER):
# get platform # get platform
notify_implementation = get_component( notify_implementation = get_component(
@ -69,7 +74,8 @@ def setup(hass, config):
# register service # register service
service_call_handler = partial(notify_message, notify_service) service_call_handler = partial(notify_message, notify_service)
service_notify = p_config.get(CONF_NAME, SERVICE_NOTIFY) service_notify = p_config.get(CONF_NAME, SERVICE_NOTIFY)
hass.services.register(DOMAIN, service_notify, service_call_handler) hass.services.register(DOMAIN, service_notify, service_call_handler,
descriptions.get(service_notify))
success = True success = True
return success return success

View File

@ -140,13 +140,19 @@ class MailNotificationService(BaseNotificationService):
self.username = username self.username = username
self.password = password self.password = password
self.recipient = recipient self.recipient = recipient
self.tries = 2
self.mail = None
self.connect()
def connect(self):
""" Connect/Authenticate to SMTP Server """
self.mail = smtplib.SMTP(self._server, self._port) self.mail = smtplib.SMTP(self._server, self._port)
self.mail.ehlo_or_helo_if_needed() self.mail.ehlo_or_helo_if_needed()
if self.starttls == 1: if self.starttls == 1:
self.mail.starttls() self.mail.starttls()
self.mail.ehlo() self.mail.ehlo()
self.mail.login(self.username, self.password) self.mail.login(self.username, self.password)
def send_message(self, message="", **kwargs): def send_message(self, message="", **kwargs):
@ -160,4 +166,12 @@ class MailNotificationService(BaseNotificationService):
msg['From'] = self._sender msg['From'] = self._sender
msg['X-Mailer'] = 'HomeAssistant' msg['X-Mailer'] = 'HomeAssistant'
self.mail.sendmail(self._sender, self.recipient, msg.as_string()) for _ in range(self.tries):
try:
self.mail.sendmail(self._sender, self.recipient,
msg.as_string())
break
except smtplib.SMTPException:
_LOGGER.warning('SMTPException sending mail: '
'retrying connection')
self.connect()

View File

@ -33,7 +33,7 @@ ATTR_ACTIVE_REQUESTED = "active_requested"
CONF_ENTITIES = "entities" CONF_ENTITIES = "entities"
SceneConfig = namedtuple('SceneConfig', ['name', 'states']) SceneConfig = namedtuple('SceneConfig', ['name', 'states', 'fuzzy_match'])
def setup(hass, config): def setup(hass, config):
@ -71,6 +71,15 @@ def setup(hass, config):
def _process_config(scene_config): def _process_config(scene_config):
""" Process passed in config into a format to work with. """ """ Process passed in config into a format to work with. """
name = scene_config.get('name') name = scene_config.get('name')
fuzzy_match = scene_config.get('fuzzy_match')
if fuzzy_match:
# default to 1%
if isinstance(fuzzy_match, int):
fuzzy_match /= 100.0
else:
fuzzy_match = 0.01
states = {} states = {}
c_entities = dict(scene_config.get(CONF_ENTITIES, {})) c_entities = dict(scene_config.get(CONF_ENTITIES, {}))
@ -91,7 +100,7 @@ def _process_config(scene_config):
states[entity_id.lower()] = State(entity_id, state, attributes) states[entity_id.lower()] = State(entity_id, state, attributes)
return SceneConfig(name, states) return SceneConfig(name, states, fuzzy_match)
class Scene(ToggleEntity): class Scene(ToggleEntity):
@ -179,9 +188,31 @@ class Scene(ToggleEntity):
state = self.scene_config.states.get(cur_state and cur_state.entity_id) state = self.scene_config.states.get(cur_state and cur_state.entity_id)
return (cur_state is not None and state.state == cur_state.state and return (cur_state is not None and state.state == cur_state.state and
all(value == cur_state.attributes.get(key) all(self._compare_state_attribites(
value, cur_state.attributes.get(key))
for key, value in state.attributes.items())) for key, value in state.attributes.items()))
def _fuzzy_attribute_compare(self, attr_a, attr_b):
"""
Compare the attributes passed, use fuzzy logic if they are floats.
"""
if not (isinstance(attr_a, float) and isinstance(attr_b, float)):
return False
diff = abs(attr_a - attr_b) / (abs(attr_a) + abs(attr_b))
return diff <= self.scene_config.fuzzy_match
def _compare_state_attribites(self, attr1, attr2):
""" Compare the attributes passed, using fuzzy logic if specified. """
if attr1 == attr2:
return True
if not self.scene_config.fuzzy_match:
return False
if isinstance(attr1, list):
return all(self._fuzzy_attribute_compare(a, b)
for a, b in zip(attr1, attr2))
return self._fuzzy_attribute_compare(attr1, attr2)
def _reproduce_state(self, states): def _reproduce_state(self, states):
""" Wraps reproduce state with Scence specific logic. """ """ Wraps reproduce state with Scence specific logic. """
self.ignore_updates = True self.ignore_updates = True

View File

@ -0,0 +1,198 @@
"""
homeassistant.components.sensor.rest
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The rest sensor will consume JSON responses sent by an exposed REST API.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.rest.html
"""
import logging
import requests
from json import loads
from datetime import timedelta
from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'REST Sensor'
DEFAULT_METHOD = 'GET'
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
# pylint: disable=unused-variable
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the REST sensor. """
use_get = False
use_post = False
resource = config.get('resource', None)
method = config.get('method', DEFAULT_METHOD)
payload = config.get('payload', None)
verify_ssl = config.get('verify_ssl', True)
if method == 'GET':
use_get = True
elif method == 'POST':
use_post = True
try:
if use_get:
response = requests.get(resource, timeout=10, verify=verify_ssl)
elif use_post:
response = requests.post(resource, data=payload, timeout=10,
verify=verify_ssl)
if not response.ok:
_LOGGER.error('Response status is "%s"', response.status_code)
return False
except requests.exceptions.MissingSchema:
_LOGGER.error('Missing resource or schema in configuration. '
'Add http:// to your URL.')
return False
except requests.exceptions.ConnectionError:
_LOGGER.error('No route to resource/endpoint. '
'Please check the URL in the configuration file.')
return False
try:
data = loads(response.text)
except ValueError:
_LOGGER.error('No valid JSON in the response in: %s', data)
return False
try:
RestSensor.extract_value(data, config.get('variable'))
except KeyError:
_LOGGER.error('Variable "%s" not found in response: "%s"',
config.get('variable'), data)
return False
if use_get:
rest = RestDataGet(resource, verify_ssl)
elif use_post:
rest = RestDataPost(resource, payload, verify_ssl)
add_devices([RestSensor(rest,
config.get('name', DEFAULT_NAME),
config.get('variable'),
config.get('unit_of_measurement'),
config.get('correction_factor', None),
config.get('decimal_places', None))])
# pylint: disable=too-many-arguments
class RestSensor(Entity):
""" Implements a REST sensor. """
def __init__(self, rest, name, variable, unit_of_measurement, corr_factor,
decimal_places):
self.rest = rest
self._name = name
self._variable = variable
self._state = 'n/a'
self._unit_of_measurement = unit_of_measurement
self._corr_factor = corr_factor
self._decimal_places = decimal_places
self.update()
@classmethod
def extract_value(cls, data, variable):
""" Extracts the value using a key name or a path. """
if isinstance(variable, list):
for variable_item in variable:
data = data[variable_item]
return data
else:
return data[variable]
@property
def name(self):
""" The name of the sensor. """
return self._name
@property
def unit_of_measurement(self):
""" Unit the value is expressed in. """
return self._unit_of_measurement
@property
def state(self):
""" Returns the state of the device. """
return self._state
def update(self):
""" Gets the latest data from REST API and updates the state. """
self.rest.update()
value = self.rest.data
if 'error' in value:
self._state = value['error']
else:
try:
if value is not None:
value = RestSensor.extract_value(value, self._variable)
if self._corr_factor is not None \
and self._decimal_places is not None:
self._state = round(
(float(value) *
float(self._corr_factor)),
self._decimal_places)
elif self._corr_factor is not None \
and self._decimal_places is None:
self._state = round(float(value) *
float(self._corr_factor))
else:
self._state = value
except ValueError:
self._state = RestSensor.extract_value(value, self._variable)
# pylint: disable=too-few-public-methods
class RestDataGet(object):
""" Class for handling the data retrieval with GET method. """
def __init__(self, resource, verify_ssl):
self._resource = resource
self._verify_ssl = verify_ssl
self.data = dict()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data from REST service with GET method. """
try:
response = requests.get(self._resource, timeout=10,
verify=self._verify_ssl)
if 'error' in self.data:
del self.data['error']
self.data = response.json()
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint.")
self.data['error'] = 'N/A'
# pylint: disable=too-few-public-methods
class RestDataPost(object):
""" Class for handling the data retrieval with POST method. """
def __init__(self, resource, payload, verify_ssl):
self._resource = resource
self._payload = payload
self._verify_ssl = verify_ssl
self.data = dict()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data from REST service with POST method. """
try:
response = requests.post(self._resource, data=self._payload,
timeout=10, verify=self._verify_ssl)
if 'error' in self.data:
del self.data['error']
self.data = response.json()
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint.")
self.data['error'] = 'N/A'

View File

@ -3,7 +3,8 @@
homeassistant.components.sensor.rpi_gpio homeassistant.components.sensor.rpi_gpio
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Allows to configure a binary state sensor using RPi GPIO. Allows to configure a binary state sensor using RPi GPIO.
Note: To use RPi GPIO, Home Assistant must be run as root. To avoid having to run Home Assistant as root when using this component,
run a Raspbian version released at or after September 29, 2015.
sensor: sensor:
platform: rpi_gpio platform: rpi_gpio

View File

@ -34,7 +34,7 @@ import homeassistant.util as util
DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit']) DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit'])
REQUIREMENTS = ['tellcore-py==1.0.4'] REQUIREMENTS = ['tellcore-py==1.1.2']
# pylint: disable=unused-argument # pylint: disable=unused-argument

View File

@ -0,0 +1,80 @@
"""
homeassistant.components.sensor.worldclock
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Worldclock sensor let you display the current time of a different time
zone.
Configuration:
To use the Worldclock sensor you will need to add something like the
following to your configuration.yaml file.
sensor:
platform: worldclock
time_zone: America/New_York
name: New York
Variables:
time_zone
*Required
Time zone you want to display.
name
*Optional
Name of the sensor to use in the frontend.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.worldclock.html
"""
import logging
import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Worldclock Sensor"
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the Worldclock sensor. """
try:
time_zone = dt_util.get_time_zone(config.get('time_zone'))
except AttributeError:
_LOGGER.error("time_zone in platform configuration is missing.")
return False
if time_zone is None:
_LOGGER.error("Timezone '%s' is not valid.", config.get('time_zone'))
return False
add_devices([WorldClockSensor(
time_zone,
config.get('name', DEFAULT_NAME)
)])
class WorldClockSensor(Entity):
""" Implements a Worldclock sensor. """
def __init__(self, time_zone, name):
self._name = name
self._time_zone = time_zone
self._state = None
self.update()
@property
def name(self):
""" Returns the name of the device. """
return self._name
@property
def state(self):
""" Returns the state of the device. """
return self._state
def update(self):
""" Gets the time and updates the states. """
self._state = dt_util.datetime_to_time_str(
dt_util.now(time_zone=self._time_zone))

View File

@ -3,9 +3,11 @@ homeassistant.components.switch
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component to interface with various switches that can be controlled remotely. Component to interface with various switches that can be controlled remotely.
""" """
import logging
from datetime import timedelta from datetime import timedelta
import logging
import os
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
@ -83,8 +85,12 @@ def setup(hass, config):
if switch.should_poll: if switch.should_poll:
switch.update_ha_state(True) switch.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service) descriptions = load_yaml_config_file(
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service) os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service,
descriptions.get(SERVICE_TURN_OFF))
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service,
descriptions.get(SERVICE_TURN_ON))
return True return True

View File

@ -11,14 +11,14 @@ signal_repetitions: 3
""" """
import logging import logging
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
from homeassistant.const import ATTR_FRIENDLY_NAME ATTR_FRIENDLY_NAME)
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
import tellcore.constants as tellcore_constants import tellcore.constants as tellcore_constants
from tellcore.library import DirectCallbackDispatcher
SINGAL_REPETITIONS = 1 SINGAL_REPETITIONS = 1
REQUIREMENTS = ['tellcore-py==1.0.4'] REQUIREMENTS = ['tellcore-py==1.1.2']
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -31,16 +31,34 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"Failed to import tellcore") "Failed to import tellcore")
return return
core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher())
signal_repetitions = config.get('signal_repetitions', SINGAL_REPETITIONS) signal_repetitions = config.get('signal_repetitions', SINGAL_REPETITIONS)
core = telldus.TelldusCore()
switches_and_lights = core.devices() switches_and_lights = core.devices()
switches = [] switches = []
for switch in switches_and_lights: for switch in switches_and_lights:
if not switch.methods(tellcore_constants.TELLSTICK_DIM): if not switch.methods(tellcore_constants.TELLSTICK_DIM):
switches.append(TellstickSwitchDevice(switch, signal_repetitions)) switches.append(
TellstickSwitchDevice(switch, signal_repetitions))
def _device_event_callback(id_, method, data, cid):
""" Called from the TelldusCore library to update one device """
for switch_device in switches:
if switch_device.tellstick_device.id == id_:
switch_device.update_ha_state()
break
callback_id = core.register_device_event(_device_event_callback)
def unload_telldus_lib(event):
""" Un-register the callback bindings """
if callback_id is not None:
core.unregister_callback(callback_id)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, unload_telldus_lib)
add_devices_callback(switches) add_devices_callback(switches)
@ -50,15 +68,20 @@ class TellstickSwitchDevice(ToggleEntity):
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF) tellcore_constants.TELLSTICK_TURNOFF)
def __init__(self, tellstick, signal_repetitions): def __init__(self, tellstick_device, signal_repetitions):
self.tellstick = tellstick self.tellstick_device = tellstick_device
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name} self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
self.signal_repetitions = signal_repetitions self.signal_repetitions = signal_repetitions
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False
@property @property
def name(self): def name(self):
""" Returns the name of the switch if any. """ """ Returns the name of the switch if any. """
return self.tellstick.name return self.tellstick_device.name
@property @property
def state_attributes(self): def state_attributes(self):
@ -68,7 +91,7 @@ class TellstickSwitchDevice(ToggleEntity):
@property @property
def is_on(self): def is_on(self):
""" True if switch is on. """ """ True if switch is on. """
last_command = self.tellstick.last_sent_command( last_command = self.tellstick_device.last_sent_command(
self.last_sent_command_mask) self.last_sent_command_mask)
return last_command == tellcore_constants.TELLSTICK_TURNON return last_command == tellcore_constants.TELLSTICK_TURNON
@ -76,9 +99,11 @@ class TellstickSwitchDevice(ToggleEntity):
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
""" Turns the switch on. """ """ Turns the switch on. """
for _ in range(self.signal_repetitions): for _ in range(self.signal_repetitions):
self.tellstick.turn_on() self.tellstick_device.turn_on()
self.update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
""" Turns the switch off. """ """ Turns the switch off. """
for _ in range(self.signal_repetitions): for _ in range(self.signal_repetitions):
self.tellstick.turn_off() self.tellstick_device.turn_off()
self.update_ha_state()

View File

@ -122,7 +122,7 @@ class VeraSwitch(ToggleEntity):
@property @property
def state_attributes(self): def state_attributes(self):
attr = super().state_attributes attr = super().state_attributes or {}
if self.vera_device.has_battery: if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'

View File

@ -9,7 +9,7 @@ import logging
from homeassistant.components.switch import SwitchDevice from homeassistant.components.switch import SwitchDevice
from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY
REQUIREMENTS = ['pywemo==0.3'] REQUIREMENTS = ['pywemo==0.3.1']
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -123,9 +123,14 @@ class WemoSwitch(SwitchDevice):
def update(self): def update(self):
""" Update WeMo state. """ """ Update WeMo state. """
self.wemo.get_state(True) try:
if self.wemo.model_name == 'Insight': self.wemo.get_state(True)
self.insight_params = self.wemo.insight_params if self.wemo.model_name == 'Insight':
self.insight_params['standby_state'] = self.wemo.get_standby_state self.insight_params = self.wemo.insight_params
elif self.wemo.model_name == 'Maker': self.insight_params['standby_state'] = (
self.maker_params = self.wemo.maker_params self.wemo.get_standby_state)
elif self.wemo.model_name == 'Maker':
self.maker_params = self.wemo.maker_params
except AttributeError:
logging.getLogger(__name__).warning(
'Could not update status for %s', self.name)

View File

@ -5,9 +5,11 @@ homeassistant.components.thermostat
Provides functionality to interact with thermostats. Provides functionality to interact with thermostats.
""" """
import logging import logging
import os
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.config import load_yaml_config_file
import homeassistant.util as util import homeassistant.util as util
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.temperature import convert from homeassistant.helpers.temperature import convert
@ -101,11 +103,16 @@ def setup(hass, config):
for thermostat in target_thermostats: for thermostat in target_thermostats:
thermostat.update_ha_state(True) thermostat.update_ha_state(True)
hass.services.register( descriptions = load_yaml_config_file(
DOMAIN, SERVICE_SET_AWAY_MODE, thermostat_service) os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register( hass.services.register(
DOMAIN, SERVICE_SET_TEMPERATURE, thermostat_service) DOMAIN, SERVICE_SET_AWAY_MODE, thermostat_service,
descriptions.get(SERVICE_SET_AWAY_MODE))
hass.services.register(
DOMAIN, SERVICE_SET_TEMPERATURE, thermostat_service,
descriptions.get(SERVICE_SET_TEMPERATURE))
return True return True

View File

@ -190,6 +190,13 @@ class HeatControl(ThermostatDevice):
if self._heater_manual_changed: if self._heater_manual_changed:
self.set_temperature(None) self.set_temperature(None)
@property
def is_away_mode_on(self):
"""
Returns if away mode is on.
"""
return self._away
def turn_away_mode_on(self): def turn_away_mode_on(self):
""" Turns away mode on. """ """ Turns away mode on. """
self._away = True self._away = True

View File

@ -0,0 +1,152 @@
"""
homeassistant.components.zone
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Allows defintion of zones in Home Assistant.
zone:
name: School
latitude: 32.8773367
longitude: -117.2494053
# Optional radius in meters (default: 100)
radius: 250
# Optional icon to show instead of name
# See https://www.google.com/design/icons/
# Example: home, work, group-work, shopping-cart, social:people
icon: group-work
zone 2:
name: Work
latitude: 32.8753367
longitude: -117.2474053
"""
import logging
from homeassistant.const import (
ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME)
from homeassistant.helpers import extract_domain_configs, generate_entity_id
from homeassistant.helpers.entity import Entity
from homeassistant.util.location import distance
DOMAIN = "zone"
DEPENDENCIES = []
ENTITY_ID_FORMAT = 'zone.{}'
ENTITY_ID_HOME = ENTITY_ID_FORMAT.format('home')
STATE = 'zoning'
DEFAULT_NAME = 'Unnamed zone'
ATTR_RADIUS = 'radius'
DEFAULT_RADIUS = 100
ATTR_ICON = 'icon'
ICON_HOME = 'home'
def active_zone(hass, latitude, longitude, radius=0):
""" Find the active zone for given latitude, longitude. """
# Sort entity IDs so that we are deterministic if equal distance to 2 zones
zones = (hass.states.get(entity_id) for entity_id
in sorted(hass.states.entity_ids(DOMAIN)))
min_dist = None
closest = None
for zone in zones:
zone_dist = distance(
latitude, longitude,
zone.attributes[ATTR_LATITUDE], zone.attributes[ATTR_LONGITUDE])
within_zone = zone_dist - radius < zone.attributes[ATTR_RADIUS]
closer_zone = closest is None or zone_dist < min_dist
smaller_zone = (zone_dist == min_dist and
zone.attributes[ATTR_RADIUS] <
closest.attributes[ATTR_RADIUS])
if within_zone and (closer_zone or smaller_zone):
min_dist = zone_dist
closest = zone
return closest
def in_zone(zone, latitude, longitude, radius=0):
""" Test if given latitude, longitude is in given zone. """
zone_dist = distance(
latitude, longitude,
zone.attributes[ATTR_LATITUDE], zone.attributes[ATTR_LONGITUDE])
return zone_dist - radius < zone.attributes[ATTR_RADIUS]
def setup(hass, config):
""" Setup zone. """
entities = set()
for key in extract_domain_configs(config, DOMAIN):
entries = config[key]
if not isinstance(entries, list):
entries = entries,
for entry in entries:
name = entry.get(CONF_NAME, DEFAULT_NAME)
latitude = entry.get(ATTR_LATITUDE)
longitude = entry.get(ATTR_LONGITUDE)
radius = entry.get(ATTR_RADIUS, DEFAULT_RADIUS)
icon = entry.get(ATTR_ICON)
if None in (latitude, longitude):
logging.getLogger(__name__).error(
'Each zone needs a latitude and longitude.')
continue
zone = Zone(hass, name, latitude, longitude, radius, icon)
zone.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name,
entities)
zone.update_ha_state()
entities.add(zone.entity_id)
if ENTITY_ID_HOME not in entities:
zone = Zone(hass, hass.config.location_name, hass.config.latitude,
hass.config.longitude, DEFAULT_RADIUS, ICON_HOME)
zone.entity_id = ENTITY_ID_HOME
zone.update_ha_state()
return True
class Zone(Entity):
""" Represents a Zone in Home Assistant. """
# pylint: disable=too-many-arguments
def __init__(self, hass, name, latitude, longitude, radius, icon):
self.hass = hass
self._name = name
self.latitude = latitude
self.longitude = longitude
self.radius = radius
self.icon = icon
def should_poll(self):
return False
@property
def name(self):
return self._name
@property
def state(self):
""" The state property really does nothing for a zone. """
return STATE
@property
def state_attributes(self):
attr = {
ATTR_HIDDEN: True,
ATTR_LATITUDE: self.latitude,
ATTR_LONGITUDE: self.longitude,
ATTR_RADIUS: self.radius,
}
if self.icon:
attr[ATTR_ICON] = self.icon
return attr

View File

@ -1,6 +1,7 @@
# coding: utf-8
""" Constants used by Home Assistant components. """ """ Constants used by Home Assistant components. """
__version__ = "0.7.3" __version__ = "0.7.4dev0"
# Can be used to specify a catch all when registering state or event listeners. # Can be used to specify a catch all when registering state or event listeners.
MATCH_ALL = '*' MATCH_ALL = '*'
@ -100,6 +101,13 @@ ATTR_LAST_TRIP_TIME = "last_tripped_time"
# For all entity's, this hold whether or not it should be hidden # For all entity's, this hold whether or not it should be hidden
ATTR_HIDDEN = "hidden" ATTR_HIDDEN = "hidden"
# Location of the entity
ATTR_LATITUDE = "latitude"
ATTR_LONGITUDE = "longitude"
# Accuracy of location in meters
ATTR_GPS_ACCURACY = 'gps_accuracy'
# #### SERVICES #### # #### SERVICES ####
SERVICE_HOMEASSISTANT_STOP = "stop" SERVICE_HOMEASSISTANT_STOP = "stop"

View File

@ -26,6 +26,7 @@ from homeassistant.exceptions import (
HomeAssistantError, InvalidEntityFormatError) HomeAssistantError, InvalidEntityFormatError)
import homeassistant.util as util import homeassistant.util as util
import homeassistant.util.dt as date_util import homeassistant.util.dt as date_util
import homeassistant.util.location as location
import homeassistant.helpers.temperature as temp_helper import homeassistant.helpers.temperature as temp_helper
from homeassistant.config import get_default_config_dir from homeassistant.config import get_default_config_dir
@ -445,9 +446,8 @@ class StateMachine(object):
domain_filter = domain_filter.lower() domain_filter = domain_filter.lower()
return [state.entity_id for key, state return [state.entity_id for state in self._states.values()
in self._states.items() if state.domain == domain_filter]
if util.split_entity_id(key)[0] == domain_filter]
def all(self): def all(self):
""" Returns a list of all states. """ """ Returns a list of all states. """
@ -524,6 +524,28 @@ class StateMachine(object):
from_state, to_state) from_state, to_state)
# pylint: disable=too-few-public-methods
class Service(object):
""" Represents a service. """
__slots__ = ['func', 'description', 'fields']
def __init__(self, func, description, fields):
self.func = func
self.description = description or ''
self.fields = fields or {}
def as_dict(self):
""" Return dictionary representation of this service. """
return {
'description': self.description,
'fields': self.fields,
}
def __call__(self, call):
self.func(call)
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class ServiceCall(object): class ServiceCall(object):
""" Represents a call to a service. """ """ Represents a call to a service. """
@ -558,20 +580,29 @@ class ServiceRegistry(object):
def services(self): def services(self):
""" Dict with per domain a list of available services. """ """ Dict with per domain a list of available services. """
with self._lock: with self._lock:
return {domain: list(self._services[domain].keys()) return {domain: {key: value.as_dict() for key, value
in self._services[domain].items()}
for domain in self._services} for domain in self._services}
def has_service(self, domain, service): def has_service(self, domain, service):
""" Returns True if specified service exists. """ """ Returns True if specified service exists. """
return service in self._services.get(domain, []) return service in self._services.get(domain, [])
def register(self, domain, service, service_func): def register(self, domain, service, service_func, description=None):
""" Register a service. """ """
Register a service.
Description is a dict containing key 'description' to describe
the service and a key 'fields' to describe the fields.
"""
description = description or {}
service_obj = Service(service_func, description.get('description'),
description.get('fields', {}))
with self._lock: with self._lock:
if domain in self._services: if domain in self._services:
self._services[domain][service] = service_func self._services[domain][service] = service_obj
else: else:
self._services[domain] = {service: service_func} self._services[domain] = {service: service_obj}
self._bus.fire( self._bus.fire(
EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REGISTERED,
@ -676,6 +707,10 @@ class Config(object):
# Directory that holds the configuration # Directory that holds the configuration
self.config_dir = get_default_config_dir() self.config_dir = get_default_config_dir()
def distance(self, lat, lon):
""" Calculate distance from Home Assistant in meters. """
return location.distance(self.latitude, self.longitude, lat, lon)
def path(self, *path): def path(self, *path):
""" Returns path to the file within the config dir. """ """ Returns path to the file within the config dir. """
return os.path.join(self.config_dir, *path) return os.path.join(self.config_dir, *path)

View File

@ -1,6 +1,8 @@
""" """
Helper methods for components within Home Assistant. Helper methods for components within Home Assistant.
""" """
import re
from homeassistant.loader import get_component from homeassistant.loader import get_component
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, DEVICE_DEFAULT_NAME) ATTR_ENTITY_ID, CONF_PLATFORM, DEVICE_DEFAULT_NAME)
@ -73,7 +75,7 @@ def config_per_platform(config, domain, logger):
config_key = domain config_key = domain
found = 1 found = 1
while config_key in config: for config_key in extract_domain_configs(config, domain):
platform_config = config[config_key] platform_config = config[config_key]
if not isinstance(platform_config, list): if not isinstance(platform_config, list):
platform_config = [platform_config] platform_config = [platform_config]
@ -89,3 +91,9 @@ def config_per_platform(config, domain, logger):
found += 1 found += 1
config_key = "{} {}".format(domain, found) config_key = "{} {}".format(domain, found)
def extract_domain_configs(config, domain):
""" Extract keys from config for given domain name. """
pattern = re.compile(r'^{}(| .+)$'.format(domain))
return (key for key in config.keys() if pattern.match(key))

View File

@ -9,7 +9,9 @@ import logging
from homeassistant.core import State from homeassistant.core import State
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF,
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
STATE_PLAYING, STATE_PAUSED, ATTR_ENTITY_ID)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -55,7 +57,11 @@ def reproduce_state(hass, states, blocking=False):
state.entity_id) state.entity_id)
continue continue
if state.state == STATE_ON: if state.domain == 'media_player' and state.state == STATE_PAUSED:
service = SERVICE_MEDIA_PAUSE
elif state.domain == 'media_player' and state.state == STATE_PLAYING:
service = SERVICE_MEDIA_PLAY
elif state.state == STATE_ON:
service = SERVICE_TURN_ON service = SERVICE_TURN_ON
elif state.state == STATE_OFF: elif state.state == STATE_OFF:
service = SERVICE_TURN_OFF service = SERVICE_TURN_OFF

View File

@ -2,6 +2,7 @@
import collections import collections
import requests import requests
from vincenty import vincenty
LocationInfo = collections.namedtuple( LocationInfo = collections.namedtuple(
@ -28,3 +29,8 @@ def detect_location_info():
'BS', 'BZ', 'KY', 'PW', 'US', 'AS', 'VI') 'BS', 'BZ', 'KY', 'PW', 'US', 'AS', 'VI')
return LocationInfo(**data) return LocationInfo(**data)
def distance(lat1, lon1, lat2, lon2):
""" Calculate the distance in meters between two points. """
return vincenty((lat1, lon1), (lat2, lon2)) * 1000

View File

@ -3,6 +3,7 @@ requests>=2,<3
pyyaml>=3.11,<4 pyyaml>=3.11,<4
pytz>=2015.4 pytz>=2015.4
pip>=7.0.0 pip>=7.0.0
vincenty==0.1.2
# Optional, needed for specific components # Optional, needed for specific components
@ -13,7 +14,7 @@ astral==0.8.1
phue==0.8 phue==0.8
# Limitlessled/Easybulb/Milight library (lights.limitlessled) # Limitlessled/Easybulb/Milight library (lights.limitlessled)
ledcontroller==1.0.7 ledcontroller==1.1.0
# Chromecast bindings (media_player.cast) # Chromecast bindings (media_player.cast)
pychromecast==0.6.12 pychromecast==0.6.12
@ -22,7 +23,7 @@ pychromecast==0.6.12
pyuserinput==0.1.9 pyuserinput==0.1.9
# Tellstick bindings (*.tellstick) # Tellstick bindings (*.tellstick)
tellcore-py==1.0.4 tellcore-py==1.1.2
# Nmap bindings (device_tracker.nmap) # Nmap bindings (device_tracker.nmap)
python-nmap==0.4.3 python-nmap==0.4.3
@ -86,10 +87,10 @@ https://github.com/theolind/pymysensors/archive/35b87d880147a34107da0d40cb815d75
pynetgear==0.3 pynetgear==0.3
# Netdisco (discovery) # Netdisco (discovery)
netdisco==0.4 netdisco==0.4.2
# Wemo (switch.wemo) # Wemo (switch.wemo)
pywemo==0.3 pywemo==0.3.1
# Wink (*.wink) # Wink (*.wink)
https://github.com/balloob/python-wink/archive/c2b700e8ca866159566ecf5e644d9c297f69f257.zip#python-wink==0.1 https://github.com/balloob/python-wink/archive/c2b700e8ca866159566ecf5e644d9c297f69f257.zip#python-wink==0.1
@ -133,3 +134,6 @@ https://github.com/balloob/home-assistant-vera-api/archive/a8f823066ead6c7da6fb5
# Sonos bindings (media_player.sonos) # Sonos bindings (media_player.sonos)
SoCo==0.11.1 SoCo==0.11.1
# PlexAPI (media_player.plex)
https://github.com/adrienbrault/python-plexapi/archive/df2d0847e801d6d5cda920326d693cf75f304f1a.zip#python-plexapi==1.0.2

View File

@ -3,6 +3,8 @@
# script/cibuild: Setup environment for CI to run tests. This is primarily # script/cibuild: Setup environment for CI to run tests. This is primarily
# designed to run on the continuous integration server. # designed to run on the continuous integration server.
cd "$(dirname "$0")/.."
script/test coverage script/test coverage
STATUS=$? STATUS=$?

View File

@ -0,0 +1,14 @@
# This is a simple service file for systems with systemd to tun HA as user.
#
[Unit]
Description=Home Assistant for %i
After=network.target
[Service]
Type=simple
User=%i
WorkingDirectory=%h
ExecStart=/usr/bin/hass --config %h/.homeassistant/
[Install]
WantedBy=multi-user.target

View File

@ -5,14 +5,15 @@ cd "$(dirname "$0")/.."
echo "Checking style with flake8..." echo "Checking style with flake8..."
flake8 --exclude www_static homeassistant flake8 --exclude www_static homeassistant
STATUS=$? FLAKE8_STATUS=$?
echo "Checking style with pylint..." echo "Checking style with pylint..."
pylint homeassistant pylint homeassistant
PYLINT_STATUS=$?
if [ $STATUS -eq 0 ] if [ $FLAKE8_STATUS -eq 0 ]
then then
exit $? exit $PYLINT_STATUS
else else
exit $STATUS exit $FLAKE8_STATUS
fi fi

21
script/release Executable file
View File

@ -0,0 +1,21 @@
# Pushes a new version to PyPi
cd "$(dirname "$0")/.."
head -n 3 homeassistant/const.py | tail -n 1 | grep dev
if [ $? -eq 0 ]
then
echo "Release version should not contain dev tag"
exit 1
fi
CURRENT_BRANCH=`git rev-parse --abbrev-ref HEAD`
if [ "$CURRENT_BRANCH" != "master" ]
then
echo "You have to be on the master branch to release."
exit 1
fi
python3 setup.py sdist bdist_wheel upload

View File

@ -7,19 +7,21 @@ cd "$(dirname "$0")/.."
script/lint script/lint
STATUS=$? LINT_STATUS=$?
echo "Running tests..." echo "Running tests..."
if [ "$1" = "coverage" ]; then if [ "$1" = "coverage" ]; then
py.test --cov --cov-report= py.test --cov --cov-report=
TEST_STATUS=$?
else else
py.test py.test
TEST_STATUS=$?
fi fi
if [ $STATUS -eq 0 ] if [ $LINT_STATUS -eq 0 ]
then then
exit $? exit $TEST_STATUS
else else
exit $STATUS exit $LINT_STATUS
fi fi

View File

@ -20,6 +20,7 @@ REQUIRES = [
'pyyaml>=3.11,<4', 'pyyaml>=3.11,<4',
'pytz>=2015.4', 'pytz>=2015.4',
'pip>=7.0.0', 'pip>=7.0.0',
'vincenty==0.1.2'
] ]
setup( setup(

View File

@ -124,14 +124,17 @@ def mock_http_component(hass):
hass.config.components.append('http') hass.config.components.append('http')
def mock_mqtt_component(hass): @mock.patch('homeassistant.components.mqtt.MQTT')
with mock.patch('homeassistant.components.mqtt.MQTT'): @mock.patch('homeassistant.components.mqtt.MQTT.publish')
mqtt.setup(hass, { def mock_mqtt_component(hass, mock_mqtt, mock_mqtt_publish):
mqtt.DOMAIN: { mqtt.setup(hass, {
mqtt.CONF_BROKER: 'mock-broker', mqtt.DOMAIN: {
} mqtt.CONF_BROKER: 'mock-broker',
}) }
hass.config.components.append(mqtt.DOMAIN) })
hass.config.components.append(mqtt.DOMAIN)
return mock_mqtt_publish
class MockHTTP(object): class MockHTTP(object):

View File

@ -11,7 +11,7 @@ import homeassistant.components.automation as automation
from tests.common import mock_mqtt_component, fire_mqtt_message from tests.common import mock_mqtt_component, fire_mqtt_message
class TestAutomationState(unittest.TestCase): class TestAutomationMQTT(unittest.TestCase):
""" Test the event automation. """ """ Test the event automation. """
def setUp(self): # pylint: disable=invalid-name def setUp(self): # pylint: disable=invalid-name

View File

@ -0,0 +1,181 @@
"""
tests.components.automation.test_location
±±±~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests location automation.
"""
import unittest
from homeassistant.components import automation, zone
from tests.common import get_test_home_assistant
class TestAutomationZone(unittest.TestCase):
""" Test the event automation. """
def setUp(self): # pylint: disable=invalid-name
self.hass = get_test_home_assistant()
zone.setup(self.hass, {
'zone': {
'name': 'test',
'latitude': 32.880837,
'longitude': -117.237561,
'radius': 250,
}
})
self.calls = []
def record_call(service):
self.calls.append(service)
self.hass.services.register('test', 'automation', record_call)
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def test_if_fires_on_zone_enter(self):
self.hass.states.set('test.entity', 'hello', {
'latitude': 32.881011,
'longitude': -117.234758
})
self.hass.pool.block_till_done()
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'zone',
'entity_id': 'test.entity',
'zone': 'zone.test',
'event': 'enter',
},
'action': {
'service': 'test.automation',
}
}
}))
self.hass.states.set('test.entity', 'hello', {
'latitude': 32.880586,
'longitude': -117.237564
})
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_not_fires_for_enter_on_zone_leave(self):
self.hass.states.set('test.entity', 'hello', {
'latitude': 32.880586,
'longitude': -117.237564
})
self.hass.pool.block_till_done()
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'zone',
'entity_id': 'test.entity',
'zone': 'zone.test',
'event': 'enter',
},
'action': {
'service': 'test.automation',
}
}
}))
self.hass.states.set('test.entity', 'hello', {
'latitude': 32.881011,
'longitude': -117.234758
})
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
def test_if_fires_on_zone_leave(self):
self.hass.states.set('test.entity', 'hello', {
'latitude': 32.880586,
'longitude': -117.237564
})
self.hass.pool.block_till_done()
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'zone',
'entity_id': 'test.entity',
'zone': 'zone.test',
'event': 'leave',
},
'action': {
'service': 'test.automation',
}
}
}))
self.hass.states.set('test.entity', 'hello', {
'latitude': 32.881011,
'longitude': -117.234758
})
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_not_fires_for_leave_on_zone_enter(self):
self.hass.states.set('test.entity', 'hello', {
'latitude': 32.881011,
'longitude': -117.234758
})
self.hass.pool.block_till_done()
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'zone',
'entity_id': 'test.entity',
'zone': 'zone.test',
'event': 'leave',
},
'action': {
'service': 'test.automation',
}
}
}))
self.hass.states.set('test.entity', 'hello', {
'latitude': 32.880586,
'longitude': -117.237564
})
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
def test_zone_condition(self):
self.hass.states.set('test.entity', 'hello', {
'latitude': 32.880586,
'longitude': -117.237564
})
self.hass.pool.block_till_done()
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'event',
'event_type': 'test_event'
},
'condition': {
'platform': 'zone',
'entity_id': 'test.entity',
'zone': 'zone.test',
},
'action': {
'service': 'test.automation',
}
}
}))
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))

View File

@ -103,12 +103,12 @@ class TestComponentsDeviceTracker(unittest.TestCase):
def test_reading_yaml_config(self): def test_reading_yaml_config(self):
dev_id = 'test' dev_id = 'test'
device = device_tracker.Device( device = device_tracker.Device(
self.hass, timedelta(seconds=180), True, dev_id, 'AB:CD:EF:GH:IJ', self.hass, timedelta(seconds=180), 0, True, dev_id,
'Test name', 'http://test.picture', True) 'AB:CD:EF:GH:IJ', 'Test name', 'http://test.picture', True)
device_tracker.update_config(self.yaml_devices, dev_id, device) device_tracker.update_config(self.yaml_devices, dev_id, device)
self.assertTrue(device_tracker.setup(self.hass, {})) self.assertTrue(device_tracker.setup(self.hass, {}))
config = device_tracker.load_config(self.yaml_devices, self.hass, config = device_tracker.load_config(self.yaml_devices, self.hass,
device.consider_home)[0] device.consider_home, 0)[0]
self.assertEqual(device.dev_id, config.dev_id) self.assertEqual(device.dev_id, config.dev_id)
self.assertEqual(device.track, config.track) self.assertEqual(device.track, config.track)
self.assertEqual(device.mac, config.mac) self.assertEqual(device.mac, config.mac)
@ -126,7 +126,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
self.assertTrue(device_tracker.setup(self.hass, { self.assertTrue(device_tracker.setup(self.hass, {
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}}))
config = device_tracker.load_config(self.yaml_devices, self.hass, config = device_tracker.load_config(self.yaml_devices, self.hass,
timedelta(seconds=0))[0] timedelta(seconds=0), 0)[0]
self.assertEqual('dev1', config.dev_id) self.assertEqual('dev1', config.dev_id)
self.assertEqual(True, config.track) self.assertEqual(True, config.track)
@ -176,7 +176,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
picture = 'http://placehold.it/200x200' picture = 'http://placehold.it/200x200'
device = device_tracker.Device( device = device_tracker.Device(
self.hass, timedelta(seconds=180), True, dev_id, None, self.hass, timedelta(seconds=180), 0, True, dev_id, None,
friendly_name, picture, away_hide=True) friendly_name, picture, away_hide=True)
device_tracker.update_config(self.yaml_devices, dev_id, device) device_tracker.update_config(self.yaml_devices, dev_id, device)
@ -191,7 +191,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
dev_id = 'test_entity' dev_id = 'test_entity'
entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
device = device_tracker.Device( device = device_tracker.Device(
self.hass, timedelta(seconds=180), True, dev_id, None, self.hass, timedelta(seconds=180), 0, True, dev_id, None,
away_hide=True) away_hide=True)
device_tracker.update_config(self.yaml_devices, dev_id, device) device_tracker.update_config(self.yaml_devices, dev_id, device)
@ -208,7 +208,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
dev_id = 'test_entity' dev_id = 'test_entity'
entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
device = device_tracker.Device( device = device_tracker.Device(
self.hass, timedelta(seconds=180), True, dev_id, None, self.hass, timedelta(seconds=180), 0, True, dev_id, None,
away_hide=True) away_hide=True)
device_tracker.update_config(self.yaml_devices, dev_id, device) device_tracker.update_config(self.yaml_devices, dev_id, device)

View File

View File

@ -0,0 +1,41 @@
"""
tests.components.sensor.test_mqtt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests mqtt sensor.
"""
import unittest
import homeassistant.core as ha
import homeassistant.components.sensor as sensor
from tests.common import mock_mqtt_component, fire_mqtt_message
class TestSensorMQTT(unittest.TestCase):
""" Test the MQTT sensor. """
def setUp(self): # pylint: disable=invalid-name
self.hass = ha.HomeAssistant()
mock_mqtt_component(self.hass)
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def test_setting_sensor_value_via_mqtt_message(self):
self.assertTrue(sensor.setup(self.hass, {
'sensor': {
'platform': 'mqtt',
'name': 'test',
'state_topic': 'test-topic',
'unit_of_measurement': 'fav unit'
}
}))
fire_mqtt_message(self.hass, 'test-topic', '100')
self.hass.pool.block_till_done()
state = self.hass.states.get('sensor.test')
self.assertEqual('100', state.state)
self.assertEqual('fav unit',
state.attributes.get('unit_of_measurement'))

View File

View File

@ -0,0 +1,82 @@
"""
tests.components.switch.test_mqtt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests mqtt switch.
"""
import unittest
from homeassistant.const import STATE_ON, STATE_OFF
import homeassistant.core as ha
import homeassistant.components.switch as switch
from tests.common import mock_mqtt_component, fire_mqtt_message
class TestSensorMQTT(unittest.TestCase):
""" Test the MQTT switch. """
def setUp(self): # pylint: disable=invalid-name
self.hass = ha.HomeAssistant()
self.mock_publish = mock_mqtt_component(self.hass)
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def test_controlling_state_via_topic(self):
self.assertTrue(switch.setup(self.hass, {
'switch': {
'platform': 'mqtt',
'name': 'test',
'state_topic': 'state-topic',
'command_topic': 'command-topic',
'payload_on': 'beer on',
'payload_off': 'beer off'
}
}))
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_OFF, state.state)
fire_mqtt_message(self.hass, 'state-topic', 'beer on')
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_ON, state.state)
fire_mqtt_message(self.hass, 'state-topic', 'beer off')
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_OFF, state.state)
def test_sending_mqtt_commands_and_optimistic(self):
self.assertTrue(switch.setup(self.hass, {
'switch': {
'platform': 'mqtt',
'name': 'test',
'command_topic': 'command-topic',
'payload_on': 'beer on',
'payload_off': 'beer off',
'qos': 2
}
}))
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_OFF, state.state)
switch.turn_on(self.hass, 'switch.test')
self.hass.pool.block_till_done()
self.assertEqual(('command-topic', 'beer on', 2),
self.mock_publish.mock_calls[-1][1])
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_ON, state.state)
switch.turn_off(self.hass, 'switch.test')
self.hass.pool.block_till_done()
self.assertEqual(('command-topic', 'beer off', 2),
self.mock_publish.mock_calls[-1][1])
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_OFF, state.state)

View File

@ -8,9 +8,8 @@ Tests component helpers.
import unittest import unittest
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.loader as loader from homeassistant import loader, helpers
from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID
from homeassistant.helpers import extract_entity_ids
from tests.common import get_test_home_assistant from tests.common import get_test_home_assistant
@ -39,10 +38,22 @@ class TestComponentsCore(unittest.TestCase):
{ATTR_ENTITY_ID: 'light.Bowl'}) {ATTR_ENTITY_ID: 'light.Bowl'})
self.assertEqual(['light.bowl'], self.assertEqual(['light.bowl'],
extract_entity_ids(self.hass, call)) helpers.extract_entity_ids(self.hass, call))
call = ha.ServiceCall('light', 'turn_on', call = ha.ServiceCall('light', 'turn_on',
{ATTR_ENTITY_ID: 'group.test'}) {ATTR_ENTITY_ID: 'group.test'})
self.assertEqual(['light.ceiling', 'light.kitchen'], self.assertEqual(['light.ceiling', 'light.kitchen'],
extract_entity_ids(self.hass, call)) helpers.extract_entity_ids(self.hass, call))
def test_extract_domain_configs(self):
config = {
'zone': None,
'zoner': None,
'zone ': None,
'zone Hallo': None,
'zone 100': None,
}
self.assertEqual(set(['zone', 'zone Hallo', 'zone 100']),
set(helpers.extract_domain_configs(config, 'zone')))

View File

@ -441,7 +441,7 @@ class TestServiceRegistry(unittest.TestCase):
def test_services(self): def test_services(self):
expected = { expected = {
'test_domain': ['test_service'] 'test_domain': {'test_service': {'description': '', 'fields': {}}}
} }
self.assertEqual(expected, self.services.services) self.assertEqual(expected, self.services.services)