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

View File

@ -1,5 +1,8 @@
sudo: false
language: python
cache:
directories:
- $HOME/virtualenv/python3.4.2/
python:
- "3.4"
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:
- 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.
- 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`.

View File

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

View File

@ -1,7 +1,9 @@
homeassistant:
# 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
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
# Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME)
# You can also have groups within groups.
group:
Home:
- group.living_room
- group.kitchen
living_room:
- light.Bowl
- light.Ceiling
- light.TV_back_light
kitchen:
- light.fan_bulb_1
- light.fan_bulb_2
children:
- device_tracker.child_1
- device_tracker.child_2
@ -94,28 +103,36 @@ browser:
keyboard:
automation:
platform: state
alias: Sun starts shining
- alias: 'Rule 1 Light on in the evening'
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
# Next two are optional, omit to match all
state_from: below_horizon
state_to: above_horizon
- alias: 'Rule 2 - Away Mode'
execute_service: light.turn_off
service_entity_id: group.living_room
trigger:
- platform: state
entity_id: group.all_devices
state: 'not_home'
automation 2:
platform: time
alias: Beer o Clock
time_hours: 16
time_minutes: 0
time_seconds: 0
execute_service: notify.notify
service_data:
message: It's 4, time for beer!
condition: use_trigger_values
action:
service: light.turn_off
entity_id: group.all_lights
sensor:
platform: systemmonitor
@ -135,6 +152,23 @@ sensor:
- type: 'process'
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:
# Turns on the bedroom lights and then the living room lights 1 minute later
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()})
# Filter out the repeating and common config section [homeassistant]
components = (key for key in config.keys()
if ' ' not in key and key != core.DOMAIN)
components = set(key.split(' ')[0] for key in config.keys()
if key != core.DOMAIN)
if not core_components.setup(hass, config):
_LOGGER.error('Home Assistant core failed to initialize. '
@ -297,11 +297,15 @@ def process_ha_core_config(hass, config):
else:
_LOGGER.error('Received invalid time zone %s', time_zone_str)
for key, attr in ((CONF_LATITUDE, 'latitude'),
(CONF_LONGITUDE, 'longitude'),
(CONF_NAME, 'location_name')):
for key, attr, typ in ((CONF_LATITUDE, 'latitude', float),
(CONF_LONGITUDE, 'longitude', float),
(CONF_NAME, 'location_name', str)):
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))

View File

@ -1,15 +1,18 @@
"""
homeassistant.components.alarm_control_panel
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component to interface with a alarm control panel.
"""
import logging
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
import os
from homeassistant.components import verisure
from homeassistant.const import (
ATTR_ENTITY_ID,
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'
DEPENDENCIES = []
@ -29,9 +32,11 @@ SERVICE_TO_METHOD = {
}
ATTR_CODE = 'code'
ATTR_CODE_FORMAT = 'code_format'
ATTR_TO_PROPERTY = [
ATTR_CODE,
ATTR_CODE_FORMAT
]
@ -57,8 +62,12 @@ def setup(hass, config):
for alarm in target_alarms:
getattr(alarm, method)(code)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
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
@ -93,16 +102,31 @@ def alarm_arm_away(hass, code, entity_id=None):
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data)
# pylint: disable=no-self-use
class AlarmControlPanel(Entity):
""" 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. """
raise NotImplementedError()
def alarm_arm_home(self, code):
def alarm_arm_home(self, code=None):
""" Send arm home command. """
raise NotImplementedError()
def alarm_arm_away(self, code):
def alarm_arm_away(self, code=None):
""" Send arm away command. """
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Interfaces with Verisure alarm control panel.
"""
import logging
@ -34,7 +34,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class VerisureAlarm(alarm.AlarmControlPanel):
""" represents a Verisure alarm status within home assistant. """
""" Represents a Verisure alarm status. """
def __init__(self, alarm_status):
self._id = alarm_status.id
@ -51,8 +51,13 @@ class VerisureAlarm(alarm.AlarmControlPanel):
""" Returns the state of the device. """
return self._state
@property
def code_format(self):
""" Four digit code required. """
return '^\\d{4}$'
def update(self):
''' update alarm status '''
""" Update alarm status """
verisure.update()
if verisure.STATUS[self._device][self._id].status == 'unarmed':
@ -66,21 +71,21 @@ class VerisureAlarm(alarm.AlarmControlPanel):
'Unknown alarm state %s',
verisure.STATUS[self._device][self._id].status)
def alarm_disarm(self, code):
def alarm_disarm(self, code=None):
""" Send disarm command. """
verisure.MY_PAGES.set_alarm_status(
code,
verisure.MY_PAGES.ALARM_DISARMED)
_LOGGER.warning('disarming')
def alarm_arm_home(self, code):
def alarm_arm_home(self, code=None):
""" Send arm home command. """
verisure.MY_PAGES.set_alarm_status(
code,
verisure.MY_PAGES.ALARM_ARMED_HOME)
_LOGGER.warning('arming home')
def alarm_arm_away(self, code):
def alarm_arm_away(self, code=None):
""" Send arm away command. """
verisure.MY_PAGES.set_alarm_status(
code,

View File

@ -103,6 +103,10 @@ def _handle_get_api_stream(handler, path_match, data):
write_lock = threading.Lock()
block = threading.Event()
restrict = data.get('restrict')
if restrict:
restrict = restrict.split(',')
def write_message(payload):
""" Writes a message to the output. """
with write_lock:
@ -118,7 +122,8 @@ def _handle_get_api_stream(handler, path_match, data):
""" Forwards events to the open request. """
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
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
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
if not hass.config.latitude:
hass.config.latitude = '32.87336'
hass.config.latitude = 32.87336
if not hass.config.longitude:
hass.config.longitude = '117.22743'
hass.config.longitude = 117.22743
bootstrap.setup_component(hass, 'sun')
@ -60,7 +60,7 @@ def setup(hass, config):
{'camera': {
'platform': 'generic',
'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
@ -108,7 +108,9 @@ def setup(hass, config):
"http://graph.facebook.com/297400035/picture",
ATTR_FRIENDLY_NAME: 'Paulus'})
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",
{

View File

@ -17,7 +17,12 @@ device_tracker:
# New found devices auto found
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
from datetime import timedelta
import logging
@ -25,7 +30,7 @@ import os
import threading
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.exceptions import HomeAssistantError
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.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"
DEPENDENCIES = []
DEPENDENCIES = ['zone']
GROUP_NAME_ALL_DEVICES = '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
CONF_CONSIDER_HOME = 'consider_home'
DEFAULT_CONF_CONSIDER_HOME = 180 # seconds
DEFAULT_CONSIDER_HOME = 180 # seconds
CONF_SCAN_INTERVAL = "interval_seconds"
DEFAULT_SCAN_INTERVAL = 12
@ -60,15 +66,17 @@ DEFAULT_SCAN_INTERVAL = 12
CONF_AWAY_HIDE = 'hide_if_away'
DEFAULT_AWAY_HIDE = False
CONF_HOME_RANGE = 'home_range'
DEFAULT_HOME_RANGE = 100
SERVICE_SEE = 'see'
ATTR_LATITUDE = 'latitude'
ATTR_LONGITUDE = 'longitude'
ATTR_MAC = 'mac'
ATTR_DEV_ID = 'dev_id'
ATTR_HOST_NAME = 'host_name'
ATTR_LOCATION_NAME = 'location_name'
ATTR_GPS = 'gps'
ATTR_BATTERY = 'battery'
DISCOVERY_PLATFORMS = {
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,
gps=None):
gps=None, gps_accuracy=None, battery=None):
""" Call service to notify you see device. """
data = {key: value for key, value in
((ATTR_MAC, mac),
@ -106,13 +114,17 @@ def setup(hass, config):
os.remove(csv_path)
conf = config.get(DOMAIN, {})
consider_home = util.convert(conf.get(CONF_CONSIDER_HOME), int,
DEFAULT_CONF_CONSIDER_HOME)
consider_home = timedelta(
seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int,
DEFAULT_CONSIDER_HOME))
track_new = util.convert(conf.get(CONF_TRACK_NEW), bool,
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))
tracker = DeviceTracker(hass, consider_home, track_new, devices)
devices = load_config(yaml_path, hass, consider_home, home_range)
tracker = DeviceTracker(hass, consider_home, track_new, home_range,
devices)
def setup_platform(p_type, p_config, disc_info=None):
""" Setup a device tracker platform. """
@ -158,22 +170,26 @@ def setup(hass, config):
""" Service to see a device. """
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_GPS)}
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY)}
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
class DeviceTracker(object):
""" 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.devices = {dev.dev_id: dev for dev in devices}
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.home_range = home_range
self.lock = threading.Lock()
for device in devices:
@ -183,7 +199,7 @@ class DeviceTracker(object):
self.group = 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. """
with self.lock:
if mac is None and dev_id is None:
@ -198,20 +214,21 @@ class DeviceTracker(object):
device = self.devices.get(dev_id)
if device:
device.seen(host_name, location_name, gps)
device.seen(host_name, location_name, gps, gps_accuracy,
battery)
if device.track:
device.update_ha_state()
return
# If no device can be found, create it
device = Device(
self.hass, self.consider_home, self.track_new, dev_id, mac,
(host_name or dev_id).replace('_', ' '))
self.hass, self.consider_home, self.home_range, self.track_new,
dev_id, mac, (host_name or dev_id).replace('_', ' '))
self.devices[dev_id] = device
if mac is not None:
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:
device.update_ha_state()
@ -239,19 +256,20 @@ class DeviceTracker(object):
class Device(Entity):
""" Tracked device. """
# pylint: disable=too-many-instance-attributes, too-many-arguments
host_name = None
location_name = None
gps = None
gps_accuracy = 0
last_seen = None
battery = None
# Track if the last update of this device was HOME
last_update_home = False
_state = STATE_NOT_HOME
def __init__(self, hass, consider_home, track, dev_id, mac, name=None,
picture=None, away_hide=False):
def __init__(self, hass, consider_home, home_range, track, dev_id, mac,
name=None, picture=None, away_hide=False):
self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
@ -259,6 +277,8 @@ class Device(Entity):
# detected anymore.
self.consider_home = consider_home
# Distance in meters
self.home_range = home_range
# Device ID
self.dev_id = dev_id
self.mac = mac
@ -273,6 +293,13 @@ class Device(Entity):
self.config_picture = picture
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
def name(self):
""" Returns the name of the entity. """
@ -292,8 +319,12 @@ class Device(Entity):
attr[ATTR_ENTITY_PICTURE] = self.config_picture
if self.gps:
attr[ATTR_LATITUDE] = self.gps[0],
attr[ATTR_LONGITUDE] = self.gps[1],
attr[ATTR_LATITUDE] = self.gps[0]
attr[ATTR_LONGITUDE] = self.gps[1]
attr[ATTR_GPS_ACCURACY] = self.gps_accuracy
if self.battery:
attr[ATTR_BATTERY] = self.battery
return attr
@ -302,12 +333,23 @@ class Device(Entity):
""" If device should be hidden. """
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. """
self.last_seen = dt_util.utcnow()
self.host_name = host_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()
def stale(self, now=None):
@ -321,6 +363,16 @@ class Device(Entity):
return
elif 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():
self._state = STATE_NOT_HOME
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(),
used_ids)
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'])
update_config(yaml_path, dev_id, device)
return True
def load_config(path, hass, consider_home):
def load_config(path, hass, consider_home, home_range):
""" Load devices from YAML config file. """
if not os.path.isfile(path):
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(),
device.get('name'), device.get('picture'),
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"
DEPENDENCIES = []
REQUIREMENTS = ['netdisco==0.4']
REQUIREMENTS = ['netdisco==0.4.2']
SCAN_INTERVAL = 300 # seconds

View File

@ -21,7 +21,8 @@ _LOGGER = logging.getLogger(__name__)
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/]+)|)')

View File

@ -1,2 +1,2 @@
""" 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
from homeassistant.const import (
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"
DEPENDENCIES = []
@ -22,7 +23,8 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
ATTR_AUTO = "auto"
# 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):

View File

@ -232,7 +232,12 @@ class RequestHandler(SimpleHTTPRequestHandler):
def log_message(self, fmt, *arguments):
""" 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
""" Does some common checks and calls appropriate method. """

View File

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

View File

@ -19,11 +19,15 @@ configuration.yaml file.
light:
platform: limitlessled
host: 192.168.1.10
group_1_name: Living Room
group_2_name: Bedroom
group_3_name: Office
group_4_name: Kitchen
bridges:
- host: 192.168.1.10
group_1_name: Living Room
group_2_name: Bedroom
group_3_name: Office
group_3_type: white
group_4_name: Kitchen
- host: 192.168.1.11
group_2_name: Basement
"""
import logging
@ -33,19 +37,30 @@ from homeassistant.components.light import (Light, ATTR_BRIGHTNESS,
from homeassistant.util.color import color_RGB_to_xy
_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):
""" Gets the LimitlessLED lights. """
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 = []
for i in range(1, 5):
if 'group_%d_name' % (i) in config:
lights.append(LimitlessLED(led, i, config['group_%d_name' % (i)]))
for bridge in bridges:
for i in range(1, 5):
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)
@ -53,15 +68,57 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
class LimitlessLED(Light):
""" Represents a LimitlessLED light """
def __init__(self, led, group, name):
self.led = led
@staticmethod
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.pool.execute(self.controller_id, "set_group_type", self.group,
group_type)
# 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._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._xy_color = color_RGB_to_xy(255, 255, 255)
@ -87,16 +144,6 @@ class LimitlessLED(Light):
((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
def brightness(self):
return self._brightness
@ -117,11 +164,6 @@ class LimitlessLED(Light):
# First candidate in the sorted list is closest to desired color:
return sorted(candidates)[0][1]
@property
def is_on(self):
""" True if device is on. """
return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
self._state = True
@ -132,12 +174,21 @@ class LimitlessLED(Light):
if ATTR_XY_COLOR in kwargs:
self._xy_color = kwargs[ATTR_XY_COLOR]
self.led.set_color(self._xy_to_led_color(self._xy_color), self.group)
self.led.set_brightness(self._brightness / 255.0, self.group)
self.pool.execute(self.controller_id, "set_color",
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()
def turn_off(self, **kwargs):
""" Turn the device off. """
self._state = False
self.led.off(self.group)
class WhiteLimitlessLED(LimitlessLED):
""" Represents a White LimitlessLED light """
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()

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
# pylint: disable=no-name-in-module, import-error
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
REQUIREMENTS = ['tellcore-py==1.0.4']
from tellcore.library import DirectCallbackDispatcher
REQUIREMENTS = ['tellcore-py==1.1.2']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return Tellstick lights. """
@ -22,13 +24,32 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"Failed to import tellcore")
return []
core = telldus.TelldusCore()
core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher())
switches_and_lights = core.devices()
lights = []
for switch in switches_and_lights:
if switch.methods(tellcore_constants.TELLSTICK_DIM):
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)
@ -40,15 +61,15 @@ class TellstickLight(Light):
tellcore_constants.TELLSTICK_UP |
tellcore_constants.TELLSTICK_DOWN)
def __init__(self, tellstick):
self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
def __init__(self, tellstick_device):
self.tellstick_device = tellstick_device
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
self._brightness = 0
@property
def name(self):
""" Returns the name of the switch if any. """
return self.tellstick.name
return self.tellstick_device.name
@property
def is_on(self):
@ -62,8 +83,9 @@ class TellstickLight(Light):
def turn_off(self, **kwargs):
""" Turns the switch off. """
self.tellstick.turn_off()
self.tellstick_device.turn_off()
self._brightness = 0
self.update_ha_state()
def turn_on(self, **kwargs):
""" Turns the switch on. """
@ -74,11 +96,12 @@ class TellstickLight(Light):
else:
self._brightness = brightness
self.tellstick.dim(self._brightness)
self.tellstick_device.dim(self._brightness)
self.update_ha_state()
def update(self):
""" 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)
if last_command == tellcore_constants.TELLSTICK_TURNON:
@ -88,6 +111,11 @@ class TellstickLight(Light):
elif (last_command == tellcore_constants.TELLSTICK_DIM or
last_command == tellcore_constants.TELLSTICK_UP or
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:
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.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)
from homeassistant import 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'))
# if last_changed == last_updated only attributes have changed
# we do not report on that yet.
# if last_changed != last_updated only attributes have changed
# we do not report on that yet. Also filter auto groups.
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
domain = to_state.domain
@ -218,10 +220,13 @@ def humanify(events):
def _entry_message_from_state(domain, state):
""" Convert a state to a message for the logbook. """
# We pass domain in so we don't have to split entity_id again
# pylint: disable=too-many-return-statements
if domain == 'device_tracker':
return '{} home'.format(
'arrived' if state.state == STATE_HOME else 'left')
if state.state == STATE_NOT_HOME:
return 'is away'
else:
return 'is at {}'.format(state.state)
elif domain == 'sun':
if state.state == sun.STATE_ABOVE_HORIZON:

View File

@ -5,8 +5,10 @@ homeassistant.components.media_player
Component to interface with various media players.
"""
import logging
import os
from homeassistant.components import discovery
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import (
@ -186,6 +188,9 @@ def setup(hass, config):
component.setup(config)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def media_player_service_handler(service):
""" Maps services to methods on MediaPlayerDevice. """
target_players = component.extract_from_service(service)
@ -199,7 +204,8 @@ def setup(hass, config):
player.update_ha_state(True)
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):
""" Set specified volume on the media player. """
@ -216,7 +222,8 @@ def setup(hass, config):
if player.should_poll:
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):
""" Mute (true) or unmute (false) the media player. """
@ -233,7 +240,8 @@ def setup(hass, config):
if player.should_poll:
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):
""" Seek to a position. """
@ -250,7 +258,8 @@ def setup(hass, config):
if player.should_poll:
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):
""" Plays specified media_id on the media player. """
@ -268,14 +277,17 @@ def setup(hass, config):
hass.services.register(
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(
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(
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service)
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service,
descriptions.get(SERVICE_YOUTUBE_VIDEO))
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
username: your_username
password: your_secret_password
certificate: /home/paulus/dev/addtrustexternalcaroot.crt
Variables:
@ -42,8 +43,13 @@ Default is a random generated one.
keepalive
*Optional
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 os
import socket
from homeassistant.exceptions import HomeAssistantError
@ -74,6 +80,7 @@ CONF_CLIENT_ID = 'client_id'
CONF_KEEPALIVE = 'keepalive'
CONF_USERNAME = 'username'
CONF_PASSWORD = 'password'
CONF_CERTIFICATE = 'certificate'
ATTR_TOPIC = 'topic'
ATTR_PAYLOAD = 'payload'
@ -119,11 +126,18 @@ def setup(hass, config):
keepalive = util.convert(conf.get(CONF_KEEPALIVE), int, DEFAULT_KEEPALIVE)
username = util.convert(conf.get(CONF_USERNAME), 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
try:
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username,
password)
password, certificate)
except socket.error:
_LOGGER.exception("Can't connect to the broker. "
"Please check your settings and the broker "
@ -161,7 +175,7 @@ def setup(hass, config):
class MQTT(object): # pragma: no cover
""" Implements messaging service for MQTT. """
def __init__(self, hass, broker, port, client_id, keepalive, username,
password):
password, certificate):
import paho.mqtt.client as mqtt
self.hass = hass
@ -172,8 +186,12 @@ class MQTT(object): # pragma: no cover
self._mqttc = mqtt.Client()
else:
self._mqttc = mqtt.Client(client_id)
if username is not None:
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_unsubscribe = self._mqtt_on_unsubscribe
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):
""" 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
self._progress = {}
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
import logging
import os
from homeassistant.config import load_yaml_config_file
from homeassistant.loader import get_component
from homeassistant.helpers import config_per_platform
@ -36,6 +38,9 @@ def setup(hass, config):
""" Sets up notify services. """
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):
# get platform
notify_implementation = get_component(
@ -69,7 +74,8 @@ def setup(hass, config):
# register service
service_call_handler = partial(notify_message, notify_service)
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
return success

View File

@ -140,13 +140,19 @@ class MailNotificationService(BaseNotificationService):
self.username = username
self.password = password
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.ehlo_or_helo_if_needed()
if self.starttls == 1:
self.mail.starttls()
self.mail.ehlo()
self.mail.login(self.username, self.password)
def send_message(self, message="", **kwargs):
@ -160,4 +166,12 @@ class MailNotificationService(BaseNotificationService):
msg['From'] = self._sender
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"
SceneConfig = namedtuple('SceneConfig', ['name', 'states'])
SceneConfig = namedtuple('SceneConfig', ['name', 'states', 'fuzzy_match'])
def setup(hass, config):
@ -71,6 +71,15 @@ def setup(hass, config):
def _process_config(scene_config):
""" Process passed in config into a format to work with. """
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 = {}
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)
return SceneConfig(name, states)
return SceneConfig(name, states, fuzzy_match)
class Scene(ToggleEntity):
@ -179,9 +188,31 @@ class Scene(ToggleEntity):
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
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()))
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):
""" Wraps reproduce state with Scence specific logic. """
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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:
platform: rpi_gpio

View File

@ -34,7 +34,7 @@ import homeassistant.util as util
DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit'])
REQUIREMENTS = ['tellcore-py==1.0.4']
REQUIREMENTS = ['tellcore-py==1.1.2']
# 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.
"""
import logging
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 import ToggleEntity
@ -83,8 +85,12 @@ def setup(hass, config):
if switch.should_poll:
switch.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service)
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service)
descriptions = load_yaml_config_file(
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

View File

@ -11,14 +11,14 @@ signal_repetitions: 3
"""
import logging
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
ATTR_FRIENDLY_NAME)
from homeassistant.helpers.entity import ToggleEntity
import tellcore.constants as tellcore_constants
from tellcore.library import DirectCallbackDispatcher
SINGAL_REPETITIONS = 1
REQUIREMENTS = ['tellcore-py==1.0.4']
REQUIREMENTS = ['tellcore-py==1.1.2']
# pylint: disable=unused-argument
@ -31,16 +31,34 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"Failed to import tellcore")
return
core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher())
signal_repetitions = config.get('signal_repetitions', SINGAL_REPETITIONS)
core = telldus.TelldusCore()
switches_and_lights = core.devices()
switches = []
for switch in switches_and_lights:
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)
@ -50,15 +68,20 @@ class TellstickSwitchDevice(ToggleEntity):
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF)
def __init__(self, tellstick, signal_repetitions):
self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
def __init__(self, tellstick_device, signal_repetitions):
self.tellstick_device = tellstick_device
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
self.signal_repetitions = signal_repetitions
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False
@property
def name(self):
""" Returns the name of the switch if any. """
return self.tellstick.name
return self.tellstick_device.name
@property
def state_attributes(self):
@ -68,7 +91,7 @@ class TellstickSwitchDevice(ToggleEntity):
@property
def is_on(self):
""" 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)
return last_command == tellcore_constants.TELLSTICK_TURNON
@ -76,9 +99,11 @@ class TellstickSwitchDevice(ToggleEntity):
def turn_on(self, **kwargs):
""" Turns the switch on. """
for _ in range(self.signal_repetitions):
self.tellstick.turn_on()
self.tellstick_device.turn_on()
self.update_ha_state()
def turn_off(self, **kwargs):
""" Turns the switch off. """
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
def state_attributes(self):
attr = super().state_attributes
attr = super().state_attributes or {}
if self.vera_device.has_battery:
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.const import STATE_ON, STATE_OFF, STATE_STANDBY
REQUIREMENTS = ['pywemo==0.3']
REQUIREMENTS = ['pywemo==0.3.1']
# pylint: disable=unused-argument
@ -123,9 +123,14 @@ class WemoSwitch(SwitchDevice):
def update(self):
""" Update WeMo state. """
self.wemo.get_state(True)
if self.wemo.model_name == 'Insight':
self.insight_params = self.wemo.insight_params
self.insight_params['standby_state'] = self.wemo.get_standby_state
elif self.wemo.model_name == 'Maker':
self.maker_params = self.wemo.maker_params
try:
self.wemo.get_state(True)
if self.wemo.model_name == 'Insight':
self.insight_params = self.wemo.insight_params
self.insight_params['standby_state'] = (
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.
"""
import logging
import os
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.config import load_yaml_config_file
import homeassistant.util as util
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.temperature import convert
@ -101,11 +103,16 @@ def setup(hass, config):
for thermostat in target_thermostats:
thermostat.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_AWAY_MODE, thermostat_service)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
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

View File

@ -190,6 +190,13 @@ class HeatControl(ThermostatDevice):
if self._heater_manual_changed:
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):
""" Turns away mode on. """
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. """
__version__ = "0.7.3"
__version__ = "0.7.4dev0"
# Can be used to specify a catch all when registering state or event listeners.
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
ATTR_HIDDEN = "hidden"
# Location of the entity
ATTR_LATITUDE = "latitude"
ATTR_LONGITUDE = "longitude"
# Accuracy of location in meters
ATTR_GPS_ACCURACY = 'gps_accuracy'
# #### SERVICES ####
SERVICE_HOMEASSISTANT_STOP = "stop"

View File

@ -26,6 +26,7 @@ from homeassistant.exceptions import (
HomeAssistantError, InvalidEntityFormatError)
import homeassistant.util as util
import homeassistant.util.dt as date_util
import homeassistant.util.location as location
import homeassistant.helpers.temperature as temp_helper
from homeassistant.config import get_default_config_dir
@ -445,9 +446,8 @@ class StateMachine(object):
domain_filter = domain_filter.lower()
return [state.entity_id for key, state
in self._states.items()
if util.split_entity_id(key)[0] == domain_filter]
return [state.entity_id for state in self._states.values()
if state.domain == domain_filter]
def all(self):
""" Returns a list of all states. """
@ -524,6 +524,28 @@ class StateMachine(object):
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
class ServiceCall(object):
""" Represents a call to a service. """
@ -558,20 +580,29 @@ class ServiceRegistry(object):
def services(self):
""" Dict with per domain a list of available services. """
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}
def has_service(self, domain, service):
""" Returns True if specified service exists. """
return service in self._services.get(domain, [])
def register(self, domain, service, service_func):
""" Register a service. """
def register(self, domain, service, service_func, description=None):
"""
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:
if domain in self._services:
self._services[domain][service] = service_func
self._services[domain][service] = service_obj
else:
self._services[domain] = {service: service_func}
self._services[domain] = {service: service_obj}
self._bus.fire(
EVENT_SERVICE_REGISTERED,
@ -676,6 +707,10 @@ class Config(object):
# Directory that holds the configuration
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):
""" Returns path to the file within the config dir. """
return os.path.join(self.config_dir, *path)

View File

@ -1,6 +1,8 @@
"""
Helper methods for components within Home Assistant.
"""
import re
from homeassistant.loader import get_component
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, DEVICE_DEFAULT_NAME)
@ -73,7 +75,7 @@ def config_per_platform(config, domain, logger):
config_key = domain
found = 1
while config_key in config:
for config_key in extract_domain_configs(config, domain):
platform_config = config[config_key]
if not isinstance(platform_config, list):
platform_config = [platform_config]
@ -89,3 +91,9 @@ def config_per_platform(config, domain, logger):
found += 1
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
import homeassistant.util.dt as dt_util
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__)
@ -55,7 +57,11 @@ def reproduce_state(hass, states, blocking=False):
state.entity_id)
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
elif state.state == STATE_OFF:
service = SERVICE_TURN_OFF

View File

@ -2,6 +2,7 @@
import collections
import requests
from vincenty import vincenty
LocationInfo = collections.namedtuple(
@ -28,3 +29,8 @@ def detect_location_info():
'BS', 'BZ', 'KY', 'PW', 'US', 'AS', 'VI')
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
pytz>=2015.4
pip>=7.0.0
vincenty==0.1.2
# Optional, needed for specific components
@ -13,7 +14,7 @@ astral==0.8.1
phue==0.8
# Limitlessled/Easybulb/Milight library (lights.limitlessled)
ledcontroller==1.0.7
ledcontroller==1.1.0
# Chromecast bindings (media_player.cast)
pychromecast==0.6.12
@ -22,7 +23,7 @@ pychromecast==0.6.12
pyuserinput==0.1.9
# Tellstick bindings (*.tellstick)
tellcore-py==1.0.4
tellcore-py==1.1.2
# Nmap bindings (device_tracker.nmap)
python-nmap==0.4.3
@ -86,10 +87,10 @@ https://github.com/theolind/pymysensors/archive/35b87d880147a34107da0d40cb815d75
pynetgear==0.3
# Netdisco (discovery)
netdisco==0.4
netdisco==0.4.2
# Wemo (switch.wemo)
pywemo==0.3
pywemo==0.3.1
# Wink (*.wink)
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)
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
# designed to run on the continuous integration server.
cd "$(dirname "$0")/.."
script/test coverage
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..."
flake8 --exclude www_static homeassistant
STATUS=$?
FLAKE8_STATUS=$?
echo "Checking style with pylint..."
pylint homeassistant
PYLINT_STATUS=$?
if [ $STATUS -eq 0 ]
if [ $FLAKE8_STATUS -eq 0 ]
then
exit $?
exit $PYLINT_STATUS
else
exit $STATUS
exit $FLAKE8_STATUS
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
STATUS=$?
LINT_STATUS=$?
echo "Running tests..."
if [ "$1" = "coverage" ]; then
py.test --cov --cov-report=
TEST_STATUS=$?
else
py.test
TEST_STATUS=$?
fi
if [ $STATUS -eq 0 ]
if [ $LINT_STATUS -eq 0 ]
then
exit $?
exit $TEST_STATUS
else
exit $STATUS
exit $LINT_STATUS
fi

View File

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

View File

@ -124,14 +124,17 @@ def mock_http_component(hass):
hass.config.components.append('http')
def mock_mqtt_component(hass):
with mock.patch('homeassistant.components.mqtt.MQTT'):
mqtt.setup(hass, {
mqtt.DOMAIN: {
mqtt.CONF_BROKER: 'mock-broker',
}
})
hass.config.components.append(mqtt.DOMAIN)
@mock.patch('homeassistant.components.mqtt.MQTT')
@mock.patch('homeassistant.components.mqtt.MQTT.publish')
def mock_mqtt_component(hass, mock_mqtt, mock_mqtt_publish):
mqtt.setup(hass, {
mqtt.DOMAIN: {
mqtt.CONF_BROKER: 'mock-broker',
}
})
hass.config.components.append(mqtt.DOMAIN)
return mock_mqtt_publish
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
class TestAutomationState(unittest.TestCase):
class TestAutomationMQTT(unittest.TestCase):
""" Test the event automation. """
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):
dev_id = 'test'
device = device_tracker.Device(
self.hass, timedelta(seconds=180), True, dev_id, 'AB:CD:EF:GH:IJ',
'Test name', 'http://test.picture', True)
self.hass, timedelta(seconds=180), 0, True, dev_id,
'AB:CD:EF:GH:IJ', 'Test name', 'http://test.picture', True)
device_tracker.update_config(self.yaml_devices, dev_id, device)
self.assertTrue(device_tracker.setup(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.track, config.track)
self.assertEqual(device.mac, config.mac)
@ -126,7 +126,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
self.assertTrue(device_tracker.setup(self.hass, {
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}}))
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(True, config.track)
@ -176,7 +176,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
picture = 'http://placehold.it/200x200'
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)
device_tracker.update_config(self.yaml_devices, dev_id, device)
@ -191,7 +191,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
dev_id = 'test_entity'
entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
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)
device_tracker.update_config(self.yaml_devices, dev_id, device)
@ -208,7 +208,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
dev_id = 'test_entity'
entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
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)
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 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.helpers import extract_entity_ids
from tests.common import get_test_home_assistant
@ -39,10 +38,22 @@ class TestComponentsCore(unittest.TestCase):
{ATTR_ENTITY_ID: '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',
{ATTR_ENTITY_ID: 'group.test'})
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):
expected = {
'test_domain': ['test_service']
'test_domain': {'test_service': {'description': '', 'fields': {}}}
}
self.assertEqual(expected, self.services.services)