Merge branch 'wink_garage_door_support' of https://github.com/xrolfex/home-assistant into wink_garage_door_support

pull/1077/head
Eric Rolf 2016-02-11 08:39:20 -05:00
commit 06cb97adee
78 changed files with 6407 additions and 3749 deletions

View File

@ -6,10 +6,14 @@ omit =
# omit pieces of code that rely on external devices being present
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/arduino.py
homeassistant/components/*/arduino.py
homeassistant/components/bloomsky.py
homeassistant/components/*/bloomsky.py
homeassistant/components/insteon_hub.py
homeassistant/components/*/insteon_hub.py
@ -102,6 +106,7 @@ omit =
homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushetta.py
homeassistant/components/notify/pushover.py
homeassistant/components/notify/rest.py
homeassistant/components/notify/slack.py
homeassistant/components/notify/smtp.py
homeassistant/components/notify/syslog.py

View File

@ -7,10 +7,12 @@ import sys
import threading
import os
import argparse
import time
from homeassistant import bootstrap
import homeassistant.config as config_util
from homeassistant.const import __version__, EVENT_HOMEASSISTANT_START
from homeassistant.const import (__version__, EVENT_HOMEASSISTANT_START,
RESTART_EXIT_CODE)
def validate_python():
@ -76,6 +78,11 @@ def get_arguments():
'--demo-mode',
action='store_true',
help='Start Home Assistant in demo mode')
parser.add_argument(
'--debug',
action='store_true',
help='Start Home Assistant in debug mode. Runs in single process to '
'enable use of interactive debuggers.')
parser.add_argument(
'--open-ui',
action='store_true',
@ -207,8 +214,11 @@ def uninstall_osx():
print("Home Assistant has been uninstalled.")
def setup_and_run_hass(config_dir, args):
""" Setup HASS and run. Block until stopped. """
def setup_and_run_hass(config_dir, args, top_process=False):
"""
Setup HASS and run. Block until stopped. Will assume it is running in a
subprocess unless top_process is set to true.
"""
if args.demo_mode:
config = {
'frontend': {},
@ -235,7 +245,11 @@ def setup_and_run_hass(config_dir, args):
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
hass.start()
sys.exit(int(hass.block_till_stopped()))
exit_code = int(hass.block_till_stopped())
if not top_process:
sys.exit(exit_code)
return exit_code
def run_hass_process(hass_proc):
@ -243,8 +257,8 @@ def run_hass_process(hass_proc):
requested_stop = threading.Event()
hass_proc.daemon = True
def request_stop():
""" request hass stop """
def request_stop(*args):
""" request hass stop, *args is for signal handler callback """
requested_stop.set()
hass_proc.terminate()
@ -262,7 +276,10 @@ def run_hass_process(hass_proc):
hass_proc.join()
except KeyboardInterrupt:
return False
return not requested_stop.isSet() and hass_proc.exitcode == 100
return (not requested_stop.isSet() and
hass_proc.exitcode == RESTART_EXIT_CODE,
hass_proc.exitcode)
def main():
@ -277,14 +294,16 @@ def main():
# os x launchd functions
if args.install_osx:
install_osx()
return
return 0
if args.uninstall_osx:
uninstall_osx()
return
return 0
if args.restart_osx:
uninstall_osx()
# A small delay is needed on some systems to let the unload finish.
time.sleep(0.5)
install_osx()
return
return 0
# daemon functions
if args.pid_file:
@ -294,12 +313,23 @@ def main():
if args.pid_file:
write_pid(args.pid_file)
# Run hass in debug mode if requested
if args.debug:
sys.stderr.write('Running in debug mode. '
'Home Assistant will not be able to restart.\n')
exit_code = setup_and_run_hass(config_dir, args, top_process=True)
if exit_code == RESTART_EXIT_CODE:
sys.stderr.write('Home Assistant requested a '
'restart in debug mode.\n')
return exit_code
# Run hass as child process. Restart if necessary.
keep_running = True
while keep_running:
hass_proc = Process(target=setup_and_run_hass, args=(config_dir, args))
keep_running = run_hass_process(hass_proc)
keep_running, exit_code = run_hass_process(hass_proc)
return exit_code
if __name__ == "__main__":
main()
sys.exit(main())

View File

@ -61,6 +61,8 @@ def setup(hass, config):
for alarm in target_alarms:
getattr(alarm, method)(code)
if alarm.should_poll:
alarm.update_ha_state(True)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))

View File

@ -90,7 +90,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
# Open another session to alarm.com to fire off the command
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
_alarm.disarm()
self.update_ha_state()
def alarm_arm_home(self, code=None):
""" Send arm home command. """
@ -100,7 +99,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
# Open another session to alarm.com to fire off the command
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
_alarm.arm_stay()
self.update_ha_state()
def alarm_arm_away(self, code=None):
""" Send arm away command. """
@ -110,7 +108,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
# Open another session to alarm.com to fire off the command
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
_alarm.arm_away()
self.update_ha_state()
def _validate_code(self, code, state):
""" Validate given code. """

View File

@ -0,0 +1,105 @@
"""
homeassistant.components.alarm_control_panel.nx584
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for NX584 alarm control panels.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.nx584/
"""
import logging
import requests
from homeassistant.const import (STATE_UNKNOWN, STATE_ALARM_DISARMED,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY)
import homeassistant.components.alarm_control_panel as alarm
REQUIREMENTS = ['pynx584==0.1']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Setup nx584. """
host = config.get('host', 'localhost:5007')
try:
add_devices([NX584Alarm(hass, host, config.get('name', 'NX584'))])
except requests.exceptions.ConnectionError as ex:
_LOGGER.error('Unable to connect to NX584: %s', str(ex))
return False
class NX584Alarm(alarm.AlarmControlPanel):
""" NX584-based alarm panel. """
def __init__(self, hass, host, name):
from nx584 import client
self._hass = hass
self._host = host
self._name = name
self._alarm = client.Client('http://%s' % host)
# Do an initial list operation so that we will try to actually
# talk to the API and trigger a requests exception for setup_platform()
# to catch
self._alarm.list_zones()
@property
def should_poll(self):
""" Polling needed. """
return True
@property
def name(self):
""" Returns the name of the device. """
return self._name
@property
def code_format(self):
""" Characters if code is defined. """
return '[0-9]{4}([0-9]{2})?'
@property
def state(self):
""" Returns the state of the device. """
try:
part = self._alarm.list_partitions()[0]
zones = self._alarm.list_zones()
except requests.exceptions.ConnectionError as ex:
_LOGGER.error('Unable to connect to %(host)s: %(reason)s',
dict(host=self._host, reason=ex))
return STATE_UNKNOWN
except IndexError:
_LOGGER.error('nx584 reports no partitions')
return STATE_UNKNOWN
bypassed = False
for zone in zones:
if zone['bypassed']:
_LOGGER.debug('Zone %(zone)s is bypassed, '
'assuming HOME',
dict(zone=zone['number']))
bypassed = True
break
if not part['armed']:
return STATE_ALARM_DISARMED
elif bypassed:
return STATE_ALARM_ARMED_HOME
else:
return STATE_ALARM_ARMED_AWAY
def alarm_disarm(self, code=None):
""" Send disarm command. """
self._alarm.disarm(code)
def alarm_arm_home(self, code=None):
""" Send arm home command. """
self._alarm.arm('home')
def alarm_arm_away(self, code=None):
""" Send arm away command. """
self._alarm.arm('auto')
def alarm_trigger(self, code=None):
""" Alarm trigger command. """
raise NotImplementedError()

View File

@ -0,0 +1,77 @@
"""
homeassistant.components.bloomsky
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for BloomSky weather station.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/bloomsky/
"""
import logging
from datetime import timedelta
import requests
from homeassistant.util import Throttle
from homeassistant.helpers import validate_config
from homeassistant.const import CONF_API_KEY
DOMAIN = "bloomsky"
BLOOMSKY = None
_LOGGER = logging.getLogger(__name__)
# The BloomSky only updates every 5-8 minutes as per the API spec so there's
# no point in polling the API more frequently
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
# pylint: disable=unused-argument,too-few-public-methods
def setup(hass, config):
""" Setup BloomSky component. """
if not validate_config(
config,
{DOMAIN: [CONF_API_KEY]},
_LOGGER):
return False
api_key = config[DOMAIN][CONF_API_KEY]
global BLOOMSKY
try:
BLOOMSKY = BloomSky(api_key)
except RuntimeError:
return False
return True
class BloomSky(object):
""" Handle all communication with the BloomSky API. """
# API documentation at http://weatherlution.com/bloomsky-api/
API_URL = "https://api.bloomsky.com/api/skydata"
def __init__(self, api_key):
self._api_key = api_key
self.devices = {}
_LOGGER.debug("Initial bloomsky device load...")
self.refresh_devices()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def refresh_devices(self):
"""
Uses the API to retreive a list of devices associated with an
account along with all the sensors on the device.
"""
_LOGGER.debug("Fetching bloomsky update")
response = requests.get(self.API_URL,
headers={"Authorization": self._api_key},
timeout=10)
if response.status_code == 401:
raise RuntimeError("Invalid API_KEY")
elif response.status_code != 200:
_LOGGER.error("Invalid HTTP response: %s", response.status_code)
return
# create dictionary keyed off of the device unique id
self.devices.update({
device["DeviceID"]: device for device in response.json()
})

View File

@ -33,8 +33,6 @@ SWITCH_ACTION_SNAPSHOT = 'snapshot'
SERVICE_CAMERA = 'camera_service'
STATE_RECORDING = 'recording'
DEFAULT_RECORDING_SECONDS = 30
# Maps discovered services to their platforms
@ -46,6 +44,7 @@ DIR_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S'
REC_DIR_PREFIX = 'recording-'
REC_IMG_PREFIX = 'recording_image-'
STATE_RECORDING = 'recording'
STATE_STREAMING = 'streaming'
STATE_IDLE = 'idle'
@ -121,33 +120,7 @@ def setup(hass, config):
try:
camera.is_streaming = True
camera.update_ha_state()
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
handler.request.sendall(bytes(
'Content-type: multipart/x-mixed-replace; \
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
# MJPEG_START_HEADER.format()
while True:
img_bytes = camera.camera_image()
if img_bytes is None:
continue
headers_str = '\r\n'.join((
'Content-length: {}'.format(len(img_bytes)),
'Content-type: image/jpeg',
)) + '\r\n\r\n'
handler.request.sendall(
bytes(headers_str, 'utf-8') +
img_bytes +
bytes('\r\n', 'utf-8'))
handler.request.sendall(
bytes('--jpgboundary\r\n', 'utf-8'))
time.sleep(0.5)
camera.mjpeg_stream(handler)
except (requests.RequestException, IOError):
camera.is_streaming = False
@ -190,6 +163,34 @@ class Camera(Entity):
""" Return bytes of camera image. """
raise NotImplementedError()
def mjpeg_stream(self, handler):
""" Generate an HTTP MJPEG stream from camera images. """
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
handler.request.sendall(bytes(
'Content-type: multipart/x-mixed-replace; \
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
# MJPEG_START_HEADER.format()
while True:
img_bytes = self.camera_image()
if img_bytes is None:
continue
headers_str = '\r\n'.join((
'Content-length: {}'.format(len(img_bytes)),
'Content-type: image/jpeg',
)) + '\r\n\r\n'
handler.request.sendall(
bytes(headers_str, 'utf-8') +
img_bytes +
bytes('\r\n', 'utf-8'))
handler.request.sendall(
bytes('--jpgboundary\r\n', 'utf-8'))
time.sleep(0.5)
@property
def state(self):
""" Returns the state of the entity. """

View File

@ -0,0 +1,60 @@
"""
homeassistant.components.camera.bloomsky
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for a camera of a BloomSky weather station.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/camera.bloomsky/
"""
import logging
import requests
import homeassistant.components.bloomsky as bloomsky
from homeassistant.components.camera import Camera
DEPENDENCIES = ["bloomsky"]
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" set up access to BloomSky cameras """
for device in bloomsky.BLOOMSKY.devices.values():
add_devices_callback([BloomSkyCamera(bloomsky.BLOOMSKY, device)])
class BloomSkyCamera(Camera):
""" Represents the images published from the BloomSky's camera. """
def __init__(self, bs, device):
""" set up for access to the BloomSky camera images """
super(BloomSkyCamera, self).__init__()
self._name = device["DeviceName"]
self._id = device["DeviceID"]
self._bloomsky = bs
self._url = ""
self._last_url = ""
# _last_image will store images as they are downloaded so that the
# frequent updates in home-assistant don't keep poking the server
# to download the same image over and over
self._last_image = ""
self._logger = logging.getLogger(__name__)
def camera_image(self):
""" Update the camera's image if it has changed. """
try:
self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"]
self._bloomsky.refresh_devices()
# if the url hasn't changed then the image hasn't changed
if self._url != self._last_url:
response = requests.get(self._url, timeout=10)
self._last_url = self._url
self._last_image = response.content
except requests.exceptions.RequestException as error:
self._logger.error("Error getting bloomsky image: %s", error)
return None
return self._last_image
@property
def name(self):
""" The name of this BloomSky device. """
return self._name

View File

@ -14,6 +14,9 @@ from requests.auth import HTTPBasicAuth
from homeassistant.helpers import validate_config
from homeassistant.components.camera import DOMAIN, Camera
from homeassistant.const import HTTP_OK
CONTENT_TYPE_HEADER = 'Content-Type'
_LOGGER = logging.getLogger(__name__)
@ -41,6 +44,17 @@ class MjpegCamera(Camera):
self._password = device_info.get('password')
self._mjpeg_url = device_info['mjpeg_url']
def camera_stream(self):
""" Return a mjpeg stream image response directly from the camera. """
if self._username and self._password:
return requests.get(self._mjpeg_url,
auth=HTTPBasicAuth(self._username,
self._password),
stream=True)
else:
return requests.get(self._mjpeg_url,
stream=True)
def camera_image(self):
""" Return a still image response from the camera. """
@ -55,16 +69,22 @@ class MjpegCamera(Camera):
jpg = data[jpg_start:jpg_end + 2]
return jpg
if self._username and self._password:
with closing(requests.get(self._mjpeg_url,
auth=HTTPBasicAuth(self._username,
self._password),
stream=True)) as response:
return process_response(response)
else:
with closing(requests.get(self._mjpeg_url,
stream=True)) as response:
return process_response(response)
with closing(self.camera_stream()) as response:
return process_response(response)
def mjpeg_stream(self, handler):
""" Generate an HTTP MJPEG stream from the camera. """
response = self.camera_stream()
content_type = response.headers[CONTENT_TYPE_HEADER]
handler.send_response(HTTP_OK)
handler.send_header(CONTENT_TYPE_HEADER, content_type)
handler.end_headers()
for chunk in response.iter_content(chunk_size=1024):
if not chunk:
break
handler.wfile.write(chunk)
@property
def name(self):

View File

@ -0,0 +1,91 @@
"""
homeassistant.components.camera.uvc
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Ubiquiti's UVC cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.uvc/
"""
import logging
import socket
import requests
from homeassistant.helpers import validate_config
from homeassistant.components.camera import DOMAIN, Camera
REQUIREMENTS = ['uvcclient==0.5']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Discover cameras on a Unifi NVR. """
if not validate_config({DOMAIN: config}, {DOMAIN: ['nvr', 'key']},
_LOGGER):
return None
addr = config.get('nvr')
port = int(config.get('port', 7080))
key = config.get('key')
from uvcclient import nvr
nvrconn = nvr.UVCRemote(addr, port, key)
try:
cameras = nvrconn.index()
except nvr.NotAuthorized:
_LOGGER.error('Authorization failure while connecting to NVR')
return False
except nvr.NvrError:
_LOGGER.error('NVR refuses to talk to me')
return False
except requests.exceptions.ConnectionError as ex:
_LOGGER.error('Unable to connect to NVR: %s', str(ex))
return False
for camera in cameras:
add_devices([UnifiVideoCamera(nvrconn,
camera['uuid'],
camera['name'])])
class UnifiVideoCamera(Camera):
""" A Ubiquiti Unifi Video Camera. """
def __init__(self, nvr, uuid, name):
super(UnifiVideoCamera, self).__init__()
self._nvr = nvr
self._uuid = uuid
self._name = name
self.is_streaming = False
@property
def name(self):
return self._name
@property
def is_recording(self):
caminfo = self._nvr.get_camera(self._uuid)
return caminfo['recordingSettings']['fullTimeRecordEnabled']
def camera_image(self):
from uvcclient import camera as uvc_camera
caminfo = self._nvr.get_camera(self._uuid)
camera = None
for addr in [caminfo['host'], caminfo['internalHost']]:
try:
camera = uvc_camera.UVCCameraClient(addr,
caminfo['username'],
'ubnt')
_LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s',
dict(name=self._name, addr=addr))
except socket.error:
pass
if not camera:
_LOGGER.error('Unable to login to camera')
return None
camera.login()
return camera.get_snapshot()

View File

@ -141,7 +141,7 @@ class Configurator(object):
state = self.hass.states.get(entity_id)
new_data = state.attributes
new_data = dict(state.attributes)
new_data[ATTR_ERRORS] = error
self.hass.states.set(entity_id, STATE_CONFIGURE, new_data)

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "b5daaa4815050f90f6c996a429bfeae1"
VERSION = "e310ed31e0c6d96def74b44c90ff5878"

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 8050e0861586a65317de27dbfd0ff50ffe209731
Subproject commit 31fb734c5b080797e81b9143e2db530c70390f5a

View File

@ -1,5 +1,5 @@
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={exports:{},id:r,loaded:!1};return e[r].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([/*!*************************************!*\
!*** ./src/service-worker/index.js ***!
\*************************************/
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){var r=fetch(e.request).then(function(e){return t.put(s,e.clone()),e});return n||r})}))})}]);
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}]);
//# sourceMappingURL=service_worker.js.map

View File

@ -71,7 +71,7 @@ def expand_entity_ids(hass, entity_ids):
if domain == DOMAIN:
found_ids.extend(
ent_id for ent_id
in get_entity_ids(hass, entity_id)
in expand_entity_ids(hass, get_entity_ids(hass, entity_id))
if ent_id not in found_ids)
else:

View File

@ -25,7 +25,7 @@ DEFAULT_DATABASE = 'home_assistant'
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = False
REQUIREMENTS = ['influxdb==2.11.0']
REQUIREMENTS = ['influxdb==2.12.0']
CONF_HOST = 'host'
CONF_PORT = 'port'
@ -70,25 +70,22 @@ def setup(hass, config):
""" Listen for new messages on the bus and sends them to Influx. """
state = event.data.get('new_state')
if state is None:
if state is None or state.state in (STATE_UNKNOWN, ''):
return
if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON):
_state = 1
elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_UNKNOWN,
STATE_BELOW_HORIZON):
elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_BELOW_HORIZON):
_state = 0
else:
_state = state.state
if _state == '':
return
try:
_state = float(_state)
_state = float(state.state)
except ValueError:
pass
_state = state.state
measurement = state.attributes.get('unit_of_measurement', state.domain)
measurement = state.attributes.get('unit_of_measurement')
if measurement in (None, ''):
measurement = state.entity_id
json_body = [
{

View File

@ -300,11 +300,6 @@ class Light(ToggleEntity):
""" CT color value in mirads. """
return None
@property
def device_state_attributes(self):
""" Returns device specific state attributes. """
return None
@property
def state_attributes(self):
""" Returns optional state attributes. """
@ -322,9 +317,4 @@ class Light(ToggleEntity):
data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1],
data[ATTR_BRIGHTNESS])
device_attr = self.device_state_attributes
if device_attr is not None:
data.update(device_attr)
return data

View File

@ -9,13 +9,20 @@ https://home-assistant.io/components/light.vera/
import logging
from requests.exceptions import RequestException
from homeassistant.components.switch.vera import VeraSwitch
import homeassistant.util.dt as dt_util
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_ON
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_TRIPPED,
ATTR_ARMED,
ATTR_LAST_TRIP_TIME,
EVENT_HOMEASSISTANT_STOP,
STATE_ON,
STATE_OFF)
REQUIREMENTS = ['pyvera==0.2.7']
REQUIREMENTS = ['pyvera==0.2.8']
_LOGGER = logging.getLogger(__name__)
@ -67,17 +74,35 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
add_devices_callback(lights)
class VeraLight(VeraSwitch):
class VeraLight(Light):
""" Represents a Vera Light, including dimmable. """
def __init__(self, vera_device, controller, extra_data=None):
self.vera_device = vera_device
self.extra_data = extra_data
self.controller = controller
if self.extra_data and self.extra_data.get('name'):
self._name = self.extra_data.get('name')
else:
self._name = self.vera_device.name
self._state = STATE_OFF
self.controller.register(vera_device, self._update_callback)
self.update()
def _update_callback(self, _device):
self.update_ha_state(True)
@property
def state_attributes(self):
attr = super().state_attributes or {}
def name(self):
""" Get the mame of the switch. """
return self._name
@property
def brightness(self):
"""Brightness of the light."""
if self.vera_device.is_dimmable:
attr[ATTR_BRIGHTNESS] = self.vera_device.get_brightness()
return attr
return self.vera_device.get_brightness()
def turn_on(self, **kwargs):
if ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable:
@ -87,3 +112,49 @@ class VeraLight(VeraSwitch):
self._state = STATE_ON
self.update_ha_state(True)
def turn_off(self, **kwargs):
self.vera_device.switch_off()
self._state = STATE_OFF
self.update_ha_state()
@property
def device_state_attributes(self):
attr = {}
if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
if self.vera_device.is_armable:
armed = self.vera_device.is_armed
attr[ATTR_ARMED] = 'True' if armed else 'False'
if self.vera_device.is_trippable:
last_tripped = self.vera_device.last_trip
if last_tripped is not None:
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str(
utc_time)
else:
attr[ATTR_LAST_TRIP_TIME] = None
tripped = self.vera_device.is_tripped
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
attr['Vera Device Id'] = self.vera_device.vera_device_id
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False
@property
def is_on(self):
""" True if device is on. """
return self._state == STATE_ON
def update(self):
""" Called by the vera device callback to update state. """
if self.vera_device.is_switched_on():
self._state = STATE_ON
else:
self._state = STATE_OFF

View File

@ -8,11 +8,10 @@ https://home-assistant.io/components/light.wink/
"""
import logging
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.wink import WinkToggleDevice
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.4.2']
REQUIREMENTS = ['python-wink==0.5.0']
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
@ -34,9 +33,32 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
WinkLight(light) for light in pywink.get_bulbs())
class WinkLight(WinkToggleDevice):
class WinkLight(Light):
""" Represents a Wink light. """
def __init__(self, wink):
self.wink = wink
@property
def unique_id(self):
""" Returns the id of this Wink switch. """
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
""" Returns the name of the light if any. """
return self.wink.name()
@property
def is_on(self):
""" True if light is on. """
return self.wink.state()
@property
def brightness(self):
"""Brightness of the light."""
return int(self.wink.brightness() * 255)
# pylint: disable=too-few-public-methods
def turn_on(self, **kwargs):
""" Turns the switch on. """
@ -48,14 +70,10 @@ class WinkLight(WinkToggleDevice):
else:
self.wink.set_state(True)
@property
def state_attributes(self):
attr = super().state_attributes
def turn_off(self):
""" Turns the switch off. """
self.wink.set_state(False)
if self.is_on:
brightness = self.wink.brightness()
if brightness is not None:
attr[ATTR_BRIGHTNESS] = int(brightness * 255)
return attr
def update(self):
""" Update state of the light. """
self.wink.update_state()

View File

@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.const import (
STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK,
ATTR_ENTITY_ID)
from homeassistant.components import (group, wink)
from homeassistant.components import (group, verisure, wink)
DOMAIN = 'lock'
SCAN_INTERVAL = 30
@ -28,12 +28,15 @@ ENTITY_ID_ALL_LOCKS = group.ENTITY_ID_FORMAT.format('all_locks')
ENTITY_ID_FORMAT = DOMAIN + '.{}'
ATTR_LOCKED = "locked"
ATTR_CODE = 'code'
ATTR_CODE_FORMAT = 'code_format'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
wink.DISCOVER_LOCKS: 'wink'
wink.DISCOVER_LOCKS: 'wink',
verisure.DISCOVER_LOCKS: 'verisure'
}
_LOGGER = logging.getLogger(__name__)
@ -45,15 +48,25 @@ def is_locked(hass, entity_id=None):
return hass.states.is_state(entity_id, STATE_LOCKED)
def lock(hass, entity_id=None):
def lock(hass, entity_id=None, code=None):
""" Locks all or specified locks. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
data = {}
if code:
data[ATTR_CODE] = code
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_LOCK, data)
def unlock(hass, entity_id=None):
def unlock(hass, entity_id=None, code=None):
""" Unlocks all or specified locks. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
data = {}
if code:
data[ATTR_CODE] = code
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_UNLOCK, data)
@ -68,11 +81,16 @@ def setup(hass, config):
""" Handles calls to the lock services. """
target_locks = component.extract_from_service(service)
if ATTR_CODE not in service.data:
code = None
else:
code = service.data[ATTR_CODE]
for item in target_locks:
if service.service == SERVICE_LOCK:
item.lock()
item.lock(code=code)
else:
item.unlock()
item.unlock(code=code)
if item.should_poll:
item.update_ha_state(True)
@ -91,19 +109,34 @@ class LockDevice(Entity):
""" Represents a lock within Home Assistant. """
# pylint: disable=no-self-use
@property
def code_format(self):
""" regex for code format or None if no code is required. """
return None
@property
def is_locked(self):
""" Is the lock locked or unlocked. """
return None
def lock(self):
def lock(self, **kwargs):
""" Locks the lock. """
raise NotImplementedError()
def unlock(self):
def unlock(self, **kwargs):
""" Unlocks the lock. """
raise NotImplementedError()
@property
def state_attributes(self):
""" Return the state attributes. """
if self.code_format is None:
return None
state_attr = {
ATTR_CODE_FORMAT: self.code_format,
}
return state_attr
@property
def state(self):
locked = self.is_locked

View File

@ -0,0 +1,92 @@
"""
homeassistant.components.lock.verisure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Interfaces with Verisure locks.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/verisure/
"""
import logging
import homeassistant.components.verisure as verisure
from homeassistant.components.lock import LockDevice
from homeassistant.const import (
STATE_UNKNOWN,
STATE_LOCKED, STATE_UNLOCKED)
_LOGGER = logging.getLogger(__name__)
ATTR_CODE = 'code'
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the Verisure platform. """
if not verisure.MY_PAGES:
_LOGGER.error('A connection has not been made to Verisure mypages.')
return False
locks = []
locks.extend([VerisureDoorlock(value)
for value in verisure.LOCK_STATUS.values()
if verisure.SHOW_LOCKS])
add_devices(locks)
# pylint: disable=abstract-method
class VerisureDoorlock(LockDevice):
""" Represents a Verisure doorlock status. """
def __init__(self, lock_status, code=None):
self._id = lock_status.id
self._state = STATE_UNKNOWN
self._code = code
@property
def name(self):
""" Returns the name of the device. """
return 'Lock {}'.format(self._id)
@property
def state(self):
""" Returns the state of the device. """
return self._state
@property
def code_format(self):
""" Six digit code required. """
return '^\\d{%s}$' % verisure.CODE_DIGITS
def update(self):
""" Update lock status """
verisure.update_lock()
if verisure.LOCK_STATUS[self._id].status == 'unlocked':
self._state = STATE_UNLOCKED
elif verisure.LOCK_STATUS[self._id].status == 'locked':
self._state = STATE_LOCKED
elif verisure.LOCK_STATUS[self._id].status != 'pending':
_LOGGER.error(
'Unknown lock state %s',
verisure.LOCK_STATUS[self._id].status)
@property
def is_locked(self):
""" True if device is locked. """
return verisure.LOCK_STATUS[self._id].status
def unlock(self, **kwargs):
""" Send unlock command. """
verisure.MY_PAGES.lock.set(kwargs[ATTR_CODE], self._id, 'UNLOCKED')
_LOGGER.info('verisure doorlock unlocking')
verisure.MY_PAGES.lock.wait_while_pending()
verisure.update_lock()
def lock(self, **kwargs):
""" Send lock command. """
verisure.MY_PAGES.lock.set(kwargs[ATTR_CODE], self._id, 'LOCKED')
_LOGGER.info('verisure doorlock locking')
verisure.MY_PAGES.lock.wait_while_pending()
verisure.update_lock()

View File

@ -11,7 +11,7 @@ import logging
from homeassistant.components.lock import LockDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.4.2']
REQUIREMENTS = ['python-wink==0.5.0']
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -425,11 +425,6 @@ class MediaPlayerDevice(Entity):
""" Flags of media commands that are supported. """
return 0
@property
def device_state_attributes(self):
""" Extra attributes a device wants to expose. """
return None
def turn_on(self):
""" turn the media player on. """
raise NotImplementedError()
@ -546,9 +541,4 @@ class MediaPlayerDevice(Entity):
if self.media_image_url:
state_attr[ATTR_ENTITY_PICTURE] = self.media_image_url
device_attr = self.device_state_attributes
if device_attr:
state_attr.update(device_attr)
return state_attr

View File

@ -56,8 +56,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hosts = [(config[CONF_HOST], DEFAULT_PORT)]
else:
hosts = [host for host in pychromecast.discover_chromecasts()
if host not in KNOWN_HOSTS]
hosts = [tuple(dev[:2]) for dev in pychromecast.discover_chromecasts()
if tuple(dev[:2]) not in KNOWN_HOSTS]
casts = []

View File

@ -12,8 +12,10 @@ import socket
import time
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
import homeassistant.util as util
from homeassistant.util import template
from homeassistant.helpers import validate_config
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
@ -49,24 +51,34 @@ DEFAULT_PROTOCOL = PROTOCOL_311
ATTR_TOPIC = 'topic'
ATTR_PAYLOAD = 'payload'
ATTR_PAYLOAD_TEMPLATE = 'payload_template'
ATTR_QOS = 'qos'
ATTR_RETAIN = 'retain'
MAX_RECONNECT_WAIT = 300 # seconds
def publish(hass, topic, payload, qos=None, retain=None):
"""Publish message to an MQTT topic."""
data = {
ATTR_TOPIC: topic,
ATTR_PAYLOAD: payload,
}
def _build_publish_data(topic, qos, retain):
"""Build the arguments for the publish service without the payload."""
data = {ATTR_TOPIC: topic}
if qos is not None:
data[ATTR_QOS] = qos
if retain is not None:
data[ATTR_RETAIN] = retain
return data
def publish(hass, topic, payload, qos=None, retain=None):
"""Publish message to an MQTT topic."""
data = _build_publish_data(topic, qos, retain)
data[ATTR_PAYLOAD] = payload
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
def publish_template(hass, topic, payload_template, qos=None, retain=None):
"""Publish message to an MQTT topic using a template payload."""
data = _build_publish_data(topic, qos, retain)
data[ATTR_PAYLOAD_TEMPLATE] = payload_template
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
@ -132,15 +144,34 @@ def setup(hass, config):
"""Handle MQTT publish service calls."""
msg_topic = call.data.get(ATTR_TOPIC)
payload = call.data.get(ATTR_PAYLOAD)
payload_template = call.data.get(ATTR_PAYLOAD_TEMPLATE)
qos = call.data.get(ATTR_QOS, DEFAULT_QOS)
retain = call.data.get(ATTR_RETAIN, DEFAULT_RETAIN)
if payload is None:
if payload_template is None:
_LOGGER.error(
"You must set either '%s' or '%s' to use this service",
ATTR_PAYLOAD, ATTR_PAYLOAD_TEMPLATE)
return
try:
payload = template.render(hass, payload_template)
except template.jinja2.TemplateError as exc:
_LOGGER.error(
"Unable to publish to '%s': rendering payload template of "
"'%s' failed because %s.",
msg_topic, payload_template, exc)
return
if msg_topic is None or payload is None:
return
MQTT_CLIENT.publish(msg_topic, payload, qos, retain)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_mqtt)
hass.services.register(DOMAIN, SERVICE_PUBLISH, publish_service)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_PUBLISH, publish_service,
descriptions.get(SERVICE_PUBLISH))
return True

View File

@ -0,0 +1,29 @@
publish:
description: Publish a message to an MQTT topic
fields:
topic:
description: Topic to publish payload
example: /homeassistant/hello
payload:
description: Payload to publish
example: This is great
payload_template:
description: Template to render as payload value. Ignored if payload given.
example: "{{ states('sensor.temperature') }}"
qos:
description: Quality of Service
example: 2
values:
- 0
- 1
- 2
default: 0
retain:
description: If message should have the retain flag set.
example: true
default: false

View File

@ -24,7 +24,9 @@ CONF_DEBUG = 'debug'
CONF_PERSISTENCE = 'persistence'
CONF_PERSISTENCE_FILE = 'persistence_file'
CONF_VERSION = 'version'
CONF_BAUD_RATE = 'baud_rate'
DEFAULT_VERSION = '1.4'
DEFAULT_BAUD_RATE = 115200
DOMAIN = 'mysensors'
DEPENDENCIES = []
@ -60,12 +62,13 @@ def setup(hass, config):
version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION))
is_metric = (hass.config.temperature_unit == TEMP_CELCIUS)
def setup_gateway(port, persistence, persistence_file, version):
def setup_gateway(port, persistence, persistence_file, version, baud_rate):
"""Return gateway after setup of the gateway."""
gateway = mysensors.SerialGateway(port, event_callback=None,
persistence=persistence,
persistence_file=persistence_file,
protocol_version=version)
protocol_version=version,
baud=baud_rate)
gateway.metric = is_metric
gateway.debug = config[DOMAIN].get(CONF_DEBUG, False)
gateway = GatewayWrapper(gateway, version)
@ -98,8 +101,9 @@ def setup(hass, config):
persistence_file = gway.get(
CONF_PERSISTENCE_FILE,
hass.config.path('mysensors{}.pickle'.format(index + 1)))
baud_rate = gway.get(CONF_BAUD_RATE, DEFAULT_BAUD_RATE)
GATEWAYS[port] = setup_gateway(
port, persistence, persistence_file, version)
port, persistence, persistence_file, version, baud_rate)
for (component, discovery_service) in DISCOVERY_COMPONENTS:
# Ensure component is loaded

View File

@ -0,0 +1,80 @@
"""
homeassistant.components.notify.rest
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
REST platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.rest/
"""
import logging
import requests
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, ATTR_TARGET, BaseNotificationService)
_LOGGER = logging.getLogger(__name__)
DEFAULT_METHOD = 'GET'
DEFAULT_MESSAGE_PARAM_NAME = 'message'
DEFAULT_TITLE_PARAM_NAME = None
DEFAULT_TARGET_PARAM_NAME = None
def get_service(hass, config):
""" Get the REST notification service. """
if not validate_config({DOMAIN: config},
{DOMAIN: ['resource', ]},
_LOGGER):
return None
method = config.get('method', DEFAULT_METHOD)
message_param_name = config.get('message_param_name',
DEFAULT_MESSAGE_PARAM_NAME)
title_param_name = config.get('title_param_name',
DEFAULT_TITLE_PARAM_NAME)
target_param_name = config.get('target_param_name',
DEFAULT_TARGET_PARAM_NAME)
return RestNotificationService(config['resource'], method,
message_param_name, title_param_name,
target_param_name)
# pylint: disable=too-few-public-methods, too-many-arguments
class RestNotificationService(BaseNotificationService):
""" Implements notification service for REST. """
def __init__(self, resource, method, message_param_name,
title_param_name, target_param_name):
self._resource = resource
self._method = method.upper()
self._message_param_name = message_param_name
self._title_param_name = title_param_name
self._target_param_name = target_param_name
def send_message(self, message="", **kwargs):
""" Send a message to a user. """
data = {
self._message_param_name: message
}
if self._title_param_name is not None:
data[self._title_param_name] = kwargs.get(ATTR_TITLE)
if self._target_param_name is not None:
data[self._title_param_name] = kwargs.get(ATTR_TARGET)
if self._method == 'POST':
response = requests.post(self._resource, data=data, timeout=10)
elif self._method == 'POST_JSON':
response = requests.post(self._resource, json=data, timeout=10)
else: # default GET
response = requests.get(self._resource, params=data, timeout=10)
if response.status_code not in (200, 201):
_LOGGER.exception(
"Error sending message. Response %d: %s:",
response.status_code, response.reason)

View File

@ -0,0 +1,238 @@
"""
homeassistant.components.proximity
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component to monitor the proximity of devices to a particular zone and the
direction of travel.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/proximity/
"""
import logging
from homeassistant.helpers.event import track_state_change
from homeassistant.helpers.entity import Entity
from homeassistant.util.location import distance
DEPENDENCIES = ['zone', 'device_tracker']
DOMAIN = 'proximity'
# Default tolerance
DEFAULT_TOLERANCE = 1
# Default zone
DEFAULT_PROXIMITY_ZONE = 'home'
# Entity attributes
ATTR_DIST_FROM = 'dist_to_zone'
ATTR_DIR_OF_TRAVEL = 'dir_of_travel'
ATTR_NEAREST = 'nearest'
ATTR_FRIENDLY_NAME = 'friendly_name'
_LOGGER = logging.getLogger(__name__)
def setup(hass, config): # pylint: disable=too-many-locals,too-many-statements
""" Get the zones and offsets from configuration.yaml. """
ignored_zones = []
if 'ignored_zones' in config[DOMAIN]:
for variable in config[DOMAIN]['ignored_zones']:
ignored_zones.append(variable)
# Get the devices from configuration.yaml
if 'devices' not in config[DOMAIN]:
_LOGGER.error('devices not found in config')
return False
proximity_devices = []
for variable in config[DOMAIN]['devices']:
proximity_devices.append(variable)
# Get the direction of travel tolerance from configuration.yaml
tolerance = config[DOMAIN].get('tolerance', DEFAULT_TOLERANCE)
# Get the zone to monitor proximity to from configuration.yaml
proximity_zone = config[DOMAIN].get('zone', DEFAULT_PROXIMITY_ZONE)
entity_id = DOMAIN + '.' + proximity_zone
proximity_zone = 'zone.' + proximity_zone
state = hass.states.get(proximity_zone)
zone_friendly_name = (state.name).lower()
# set the default values
dist_to_zone = 'not set'
dir_of_travel = 'not set'
nearest = 'not set'
proximity = Proximity(hass, zone_friendly_name, dist_to_zone,
dir_of_travel, nearest, ignored_zones,
proximity_devices, tolerance, proximity_zone)
proximity.entity_id = entity_id
proximity.update_ha_state()
# Main command to monitor proximity of devices
track_state_change(hass, proximity_devices,
proximity.check_proximity_state_change)
return True
class Proximity(Entity): # pylint: disable=too-many-instance-attributes
""" Represents a Proximity. """
def __init__(self, hass, zone_friendly_name, dist_to, dir_of_travel,
nearest, ignored_zones, proximity_devices, tolerance,
proximity_zone):
# pylint: disable=too-many-arguments
self.hass = hass
self.friendly_name = zone_friendly_name
self.dist_to = dist_to
self.dir_of_travel = dir_of_travel
self.nearest = nearest
self.ignored_zones = ignored_zones
self.proximity_devices = proximity_devices
self.tolerance = tolerance
self.proximity_zone = proximity_zone
@property
def state(self):
""" Returns the state. """
return self.dist_to
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity. """
return "km"
@property
def state_attributes(self):
""" Returns the state attributes. """
return {
ATTR_DIR_OF_TRAVEL: self.dir_of_travel,
ATTR_NEAREST: self.nearest,
ATTR_FRIENDLY_NAME: self.friendly_name
}
def check_proximity_state_change(self, entity, old_state, new_state):
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
""" Function to perform the proximity checking. """
entity_name = new_state.name
devices_to_calculate = False
devices_in_zone = ''
zone_state = self.hass.states.get(self.proximity_zone)
proximity_latitude = zone_state.attributes.get('latitude')
proximity_longitude = zone_state.attributes.get('longitude')
# Check for devices in the monitored zone
for device in self.proximity_devices:
device_state = self.hass.states.get(device)
if device_state.state not in self.ignored_zones:
devices_to_calculate = True
# Check the location of all devices
if (device_state.state).lower() == (self.friendly_name).lower():
device_friendly = device_state.name
if devices_in_zone != '':
devices_in_zone = devices_in_zone + ', '
devices_in_zone = devices_in_zone + device_friendly
# No-one to track so reset the entity
if not devices_to_calculate:
self.dist_to = 'not set'
self.dir_of_travel = 'not set'
self.nearest = 'not set'
self.update_ha_state()
return
# At least one device is in the monitored zone so update the entity
if devices_in_zone != '':
self.dist_to = 0
self.dir_of_travel = 'arrived'
self.nearest = devices_in_zone
self.update_ha_state()
return
# We can't check proximity because latitude and longitude don't exist
if 'latitude' not in new_state.attributes:
return
# Collect distances to the zone for all devices
distances_to_zone = {}
for device in self.proximity_devices:
# Ignore devices in an ignored zone
device_state = self.hass.states.get(device)
if device_state.state in self.ignored_zones:
continue
# Ignore devices if proximity cannot be calculated
if 'latitude' not in device_state.attributes:
continue
# Calculate the distance to the proximity zone
dist_to_zone = distance(proximity_latitude,
proximity_longitude,
device_state.attributes['latitude'],
device_state.attributes['longitude'])
# Add the device and distance to a dictionary
distances_to_zone[device] = round(dist_to_zone / 1000, 1)
# Loop through each of the distances collected and work out the closest
closest_device = ''
dist_to_zone = 1000000
for device in distances_to_zone:
if distances_to_zone[device] < dist_to_zone:
closest_device = device
dist_to_zone = distances_to_zone[device]
# If the closest device is one of the other devices
if closest_device != entity:
self.dist_to = round(distances_to_zone[closest_device])
self.dir_of_travel = 'unknown'
device_state = self.hass.states.get(closest_device)
self.nearest = device_state.name
self.update_ha_state()
return
# Stop if we cannot calculate the direction of travel (i.e. we don't
# have a previous state and a current LAT and LONG)
if old_state is None or 'latitude' not in old_state.attributes:
self.dist_to = round(distances_to_zone[entity])
self.dir_of_travel = 'unknown'
self.nearest = entity_name
self.update_ha_state()
return
# Reset the variables
distance_travelled = 0
# Calculate the distance travelled
old_distance = distance(proximity_latitude, proximity_longitude,
old_state.attributes['latitude'],
old_state.attributes['longitude'])
new_distance = distance(proximity_latitude, proximity_longitude,
new_state.attributes['latitude'],
new_state.attributes['longitude'])
distance_travelled = round(new_distance - old_distance, 1)
# Check for tolerance
if distance_travelled < self.tolerance * -1:
direction_of_travel = 'towards'
elif distance_travelled > self.tolerance:
direction_of_travel = 'away_from'
else:
direction_of_travel = 'stationary'
# Update the proximity entity
self.dist_to = round(dist_to_zone)
self.dir_of_travel = direction_of_travel
self.nearest = entity_name
self.update_ha_state()
_LOGGER.debug('proximity.%s update entity: distance=%s: direction=%s: '
'device=%s', self.friendly_name, round(dist_to_zone),
direction_of_travel, entity_name)
_LOGGER.info('%s: proximity calculation complete', entity_name)

View File

@ -232,7 +232,7 @@ class Recorder(threading.Thread):
else:
state_domain = state.domain
state_state = state.state
state_attr = json.dumps(state.attributes)
state_attr = json.dumps(dict(state.attributes))
last_changed = state.last_changed
last_updated = state.last_updated
@ -285,7 +285,8 @@ class Recorder(threading.Thread):
else:
return cur.fetchall()
except sqlite3.IntegrityError:
except (sqlite3.IntegrityError, sqlite3.OperationalError,
sqlite3.ProgrammingError):
_LOGGER.exception(
"Error querying the database using: %s", sql_query)
return []
@ -329,7 +330,7 @@ class Recorder(threading.Thread):
migration_id = 0
if migration_id < 1:
self.query("""
cur.execute("""
CREATE TABLE recorder_runs (
run_id integer primary key,
start integer,
@ -338,7 +339,7 @@ class Recorder(threading.Thread):
created integer)
""")
self.query("""
cur.execute("""
CREATE TABLE events (
event_id integer primary key,
event_type text,
@ -346,10 +347,10 @@ class Recorder(threading.Thread):
origin text,
created integer)
""")
self.query(
cur.execute(
'CREATE INDEX events__event_type ON events(event_type)')
self.query("""
cur.execute("""
CREATE TABLE states (
state_id integer primary key,
entity_id text,
@ -359,51 +360,51 @@ class Recorder(threading.Thread):
last_updated integer,
created integer)
""")
self.query('CREATE INDEX states__entity_id ON states(entity_id)')
cur.execute('CREATE INDEX states__entity_id ON states(entity_id)')
save_migration(1)
if migration_id < 2:
self.query("""
cur.execute("""
ALTER TABLE events
ADD COLUMN time_fired integer
""")
self.query('UPDATE events SET time_fired=created')
cur.execute('UPDATE events SET time_fired=created')
save_migration(2)
if migration_id < 3:
utc_offset = self.utc_offset
self.query("""
cur.execute("""
ALTER TABLE recorder_runs
ADD COLUMN utc_offset integer
""")
self.query("""
cur.execute("""
ALTER TABLE events
ADD COLUMN utc_offset integer
""")
self.query("""
cur.execute("""
ALTER TABLE states
ADD COLUMN utc_offset integer
""")
self.query("UPDATE recorder_runs SET utc_offset=?", [utc_offset])
self.query("UPDATE events SET utc_offset=?", [utc_offset])
self.query("UPDATE states SET utc_offset=?", [utc_offset])
cur.execute("UPDATE recorder_runs SET utc_offset=?", [utc_offset])
cur.execute("UPDATE events SET utc_offset=?", [utc_offset])
cur.execute("UPDATE states SET utc_offset=?", [utc_offset])
save_migration(3)
if migration_id < 4:
# We had a bug where we did not save utc offset for recorder runs
self.query(
cur.execute(
"""UPDATE recorder_runs SET utc_offset=?
WHERE utc_offset IS NULL""", [self.utc_offset])
self.query("""
cur.execute("""
ALTER TABLE states
ADD COLUMN event_id integer
""")
@ -412,25 +413,33 @@ class Recorder(threading.Thread):
if migration_id < 5:
# Add domain so that thermostat graphs look right
self.query("""
ALTER TABLE states
ADD COLUMN domain text
""")
try:
cur.execute("""
ALTER TABLE states
ADD COLUMN domain text
""")
except sqlite3.OperationalError:
# We had a bug in this migration for a while on dev
# Without this, dev-users will have to throw away their db
pass
# TravisCI has Python compiled against an old version of SQLite3
# which misses the instr method.
self.conn.create_function(
"instr", 2,
lambda string, substring: string.find(substring) + 1)
# populate domain with defaults
rows = self.query("select distinct entity_id from states")
for row in rows:
entity_id = row[0]
domain = entity_id.split(".")[0]
self.query(
"UPDATE states set domain=? where entity_id=?",
domain, entity_id)
cur.execute("""
UPDATE states
set domain=substr(entity_id, 0, instr(entity_id, '.'))
""")
# add indexes we are going to use a lot on selects
self.query("""
cur.execute("""
CREATE INDEX states__state_changes ON
states (last_changed, last_updated, entity_id)""")
self.query("""
cur.execute("""
CREATE INDEX states__significant_changes ON
states (domain, last_updated, entity_id)""")
save_migration(5)

View File

@ -39,6 +39,7 @@ OPTION_TYPES = {
'miners_revenue_btc': ['Miners revenue', 'BTC'],
'market_price_usd': ['Market price', 'USD']
}
ICON = 'mdi:currency-btc'
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
@ -108,6 +109,11 @@ class BitcoinSensor(Entity):
def unit_of_measurement(self):
return self._unit_of_measurement
@property
def icon(self):
""" Icon to use in the frontend, if any. """
return ICON
# pylint: disable=too-many-branches
def update(self):
""" Gets the latest data and updates the states. """

View File

@ -0,0 +1,104 @@
"""
homeassistant.components.sensor.bloomsky
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support the sensor of a BloomSky weather station.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/sensor.bloomsky/
"""
import logging
import homeassistant.components.bloomsky as bloomsky
from homeassistant.helpers.entity import Entity
DEPENDENCIES = ["bloomsky"]
# These are the available sensors
SENSOR_TYPES = ["Temperature",
"Humidity",
"Rain",
"Pressure",
"Luminance",
"Night",
"UVIndex"]
# Sensor units - these do not currently align with the API documentation
SENSOR_UNITS = {"Temperature": "°F",
"Humidity": "%",
"Pressure": "inHg",
"Luminance": "cd/m²"}
# Which sensors to format numerically
FORMAT_NUMBERS = ["Temperature", "Pressure"]
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Set up the available BloomSky weather sensors. """
logger = logging.getLogger(__name__)
for device_key in bloomsky.BLOOMSKY.devices:
device = bloomsky.BLOOMSKY.devices[device_key]
for variable in config["monitored_conditions"]:
if variable in SENSOR_TYPES:
add_devices([BloomSkySensor(bloomsky.BLOOMSKY,
device,
variable)])
else:
logger.error("Cannot find definition for device: %s", variable)
class BloomSkySensor(Entity):
""" Represents a single sensor in a BloomSky device. """
def __init__(self, bs, device, sensor_name):
self._bloomsky = bs
self._device_id = device["DeviceID"]
self._client_name = device["DeviceName"]
self._sensor_name = sensor_name
self._state = self.process_state(device)
self._sensor_update = ""
@property
def name(self):
""" The name of the BloomSky device and this sensor. """
return "{} {}".format(self._client_name, self._sensor_name)
@property
def state(self):
""" The current state (i.e. value) of this sensor. """
return self._state
@property
def unit_of_measurement(self):
""" This sensor's units. """
return SENSOR_UNITS.get(self._sensor_name, None)
def update(self):
""" Request an update from the BloomSky API. """
self._bloomsky.refresh_devices()
# TS is a Unix epoch timestamp for the last time the BloomSky servers
# heard from this device. If that value hasn't changed, the value has
# not been updated.
last_ts = self._bloomsky.devices[self._device_id]["Data"]["TS"]
if last_ts != self._sensor_update:
self.process_state(self._bloomsky.devices[self._device_id])
self._sensor_update = last_ts
def process_state(self, device):
""" Handle the response from the BloomSky API for this sensor. """
data = device["Data"][self._sensor_name]
if self._sensor_name == "Rain":
if data:
self._state = "Raining"
else:
self._state = "Not raining"
elif self._sensor_name == "Night":
if data:
self._state = "Nighttime"
else:
self._state = "Daytime"
elif self._sensor_name in FORMAT_NUMBERS:
self._state = "{0:.2f}".format(data)
else:
self._state = data

View File

@ -54,7 +54,7 @@ class CpuSpeedSensor(Entity):
return self._unit_of_measurement
@property
def state_attributes(self):
def device_state_attributes(self):
""" Returns the state attributes. """
if self.info is not None:
return {

View File

@ -46,7 +46,7 @@ class DemoSensor(Entity):
return self._unit_of_measurement
@property
def state_attributes(self):
def device_state_attributes(self):
""" Returns the state attributes. """
if self._battery:
return {

View File

@ -23,6 +23,7 @@ SENSOR_TYPES = {
'temperature': ['Temperature', None],
'humidity': ['Humidity', '%']
}
DEFAULT_NAME = "DHT Sensor"
# Return cached results if last scan was less then this time ago
# DHT11 is able to deliver data once per second, DHT22 once every two
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
@ -53,12 +54,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
data = DHTClient(Adafruit_DHT, sensor, pin)
dev = []
name = config.get('name', DEFAULT_NAME)
try:
for variable in config['monitored_conditions']:
if variable not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable)
else:
dev.append(DHTSensor(data, variable, unit))
dev.append(DHTSensor(data, variable, unit, name))
except KeyError:
pass
@ -69,8 +72,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DHTSensor(Entity):
""" Implements an DHT sensor. """
def __init__(self, dht_client, sensor_type, temp_unit):
self.client_name = 'DHT sensor'
def __init__(self, dht_client, sensor_type, temp_unit, name):
self.client_name = name
self._name = SENSOR_TYPES[sensor_type][0]
self.dht_client = dht_client
self.temp_unit = temp_unit

View File

@ -63,6 +63,11 @@ class EcobeeSensor(Entity):
""" Returns the state of the device. """
return self._state
@property
def unique_id(self):
"""Unique id of this sensor."""
return "sensor_ecobee_{}_{}".format(self.type, self.index)
@property
def unit_of_measurement(self):
""" Unit of measurement this sensor expresses itself in. """

View File

@ -0,0 +1,99 @@
"""
homeassistant.components.sensor.mfi
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Ubiquiti mFi sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.mfi/
"""
import logging
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD,
TEMP_CELCIUS)
from homeassistant.components.sensor import DOMAIN
from homeassistant.helpers.entity import Entity
from homeassistant.helpers import validate_config
REQUIREMENTS = ['mficlient==0.2.2']
_LOGGER = logging.getLogger(__name__)
STATE_ON = 'on'
STATE_OFF = 'off'
DIGITS = {
'volts': 1,
'amps': 1,
'active_power': 0,
'temperature': 1,
}
SENSOR_MODELS = [
'Ubiquiti mFi-THS',
'Ubiquiti mFi-CS',
'Outlet',
'Input Analog',
'Input Digital',
]
# pylint: disable=unused-variable
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up mFi sensors. """
if not validate_config({DOMAIN: config},
{DOMAIN: ['host',
CONF_USERNAME,
CONF_PASSWORD]},
_LOGGER):
_LOGGER.error('A host, username, and password are required')
return False
host = config.get('host')
port = int(config.get('port', 6443))
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
from mficlient.client import MFiClient
try:
client = MFiClient(host, username, password, port=port)
except client.FailedToLogin as ex:
_LOGGER.error('Unable to connect to mFi: %s', str(ex))
return False
add_devices(MfiSensor(port, hass)
for device in client.get_devices()
for port in device.ports.values()
if port.model in SENSOR_MODELS)
class MfiSensor(Entity):
""" An mFi sensor that exposes tag=value. """
def __init__(self, port, hass):
self._port = port
self._hass = hass
@property
def name(self):
return self._port.label
@property
def state(self):
if self._port.model == 'Input Digital':
return self._port.value > 0 and STATE_ON or STATE_OFF
else:
digits = DIGITS.get(self._port.tag, 0)
return round(self._port.value, digits)
@property
def unit_of_measurement(self):
if self._port.tag == 'temperature':
return TEMP_CELCIUS
elif self._port.tag == 'active_pwr':
return 'Watts'
elif self._port.model == 'Input Digital':
return 'State'
return self._port.tag
def update(self):
self._port.refresh()

View File

@ -7,7 +7,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.mqtt/
"""
import logging
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.const import CONF_VALUE_TEMPLATE, STATE_UNKNOWN
from homeassistant.helpers.entity import Entity
from homeassistant.util import template
import homeassistant.components.mqtt as mqtt
@ -42,7 +42,7 @@ class MqttSensor(Entity):
""" Represents a sensor that can be updated using MQTT. """
def __init__(self, hass, name, state_topic, qos, unit_of_measurement,
value_template):
self._state = "-"
self._state = STATE_UNKNOWN
self._hass = hass
self._name = name
self._state_topic = state_topic

View File

@ -151,41 +151,39 @@ class MySensorsSensor(Entity):
self.gateway.const.SetReq.V_VOLTAGE: 'V',
self.gateway.const.SetReq.V_CURRENT: 'A',
}
unit_map_v15 = {
self.gateway.const.SetReq.V_PERCENTAGE: '%',
}
if float(self.gateway.version) >= 1.5:
if self.gateway.const.SetReq.V_UNIT_PREFIX in self._values:
return self._values[
self.gateway.const.SetReq.V_UNIT_PREFIX]
unit_map.update(unit_map_v15)
unit_map.update({self.gateway.const.SetReq.V_PERCENTAGE: '%'})
return unit_map.get(self.value_type)
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
device_attr = {}
for value_type, value in self._values.items():
if value_type != self.value_type:
device_attr[self.gateway.const.SetReq(value_type).name] = value
return device_attr
@property
def state_attributes(self):
"""Return the state attributes."""
data = {
attr = {
mysensors.ATTR_PORT: self.gateway.port,
mysensors.ATTR_NODE_ID: self.node_id,
mysensors.ATTR_CHILD_ID: self.child_id,
ATTR_BATTERY_LEVEL: self.battery_level,
}
device_attr = self.device_state_attributes
set_req = self.gateway.const.SetReq
if device_attr is not None:
data.update(device_attr)
for value_type, value in self._values.items():
if value_type != self.value_type:
try:
attr[set_req(value_type).name] = value
except ValueError:
_LOGGER.error('value_type %s is not valid for mysensors '
'version %s', value_type,
self.gateway.version)
return attr
return data
@property
def available(self):
"""Return True if entity is available."""
return self.value_type in self._values
def update(self):
"""Update the controller with the latest values from a sensor."""

View File

@ -21,7 +21,7 @@ SENSOR_TYPES = ['humidity',
'last_connection',
'battery_level']
SENSOR_UNITS = {'humidity': '%', 'battery_level': '%'}
SENSOR_UNITS = {'humidity': '%', 'battery_level': 'V'}
SENSOR_TEMP_TYPES = ['temperature',
'target',

View File

@ -87,7 +87,7 @@ class RfxtrxSensor(Entity):
return self._name
@property
def state_attributes(self):
def device_state_attributes(self):
return self.event.values
@property

View File

@ -75,7 +75,7 @@ class SwissPublicTransportSensor(Entity):
return self._state
@property
def state_attributes(self):
def device_state_attributes(self):
""" Returns the state attributes. """
if self._times is not None:
return {

View File

@ -113,8 +113,8 @@ class TelldusLiveSensor(Entity):
return self._value_as_humidity
@property
def state_attributes(self):
attrs = dict()
def device_state_attributes(self):
attrs = {}
if self._battery_level is not None:
attrs[ATTR_BATTERY_LEVEL] = self._battery_level
if self._last_updated is not None:

View File

@ -94,10 +94,7 @@ class SensorTemplate(Entity):
def _update_callback(_event):
""" Called when the target device changes state. """
# This can be called before the entity is properly
# initialised, so check before updating state,
if self.entity_id:
self.update_ha_state(True)
self.update_ha_state(True)
self.hass.bus.listen(EVENT_STATE_CHANGED, _update_callback)

View File

@ -67,7 +67,7 @@ class TwitchSensor(Entity):
self._state = STATE_OFFLINE
@property
def state_attributes(self):
def device_state_attributes(self):
""" Returns the state attributes. """
if self._state == STATE_STREAMING:
return {

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME,
TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = ['pyvera==0.2.7']
REQUIREMENTS = ['pyvera==0.2.8']
_LOGGER = logging.getLogger(__name__)
@ -118,7 +118,7 @@ class VeraSensor(Entity):
return '%'
@property
def state_attributes(self):
def device_state_attributes(self):
attr = {}
if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'

View File

@ -11,7 +11,7 @@ import logging
from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED
REQUIREMENTS = ['python-wink==0.4.2']
REQUIREMENTS = ['python-wink==0.5.0']
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -100,7 +100,7 @@ class YrSensor(Entity):
return self._state
@property
def state_attributes(self):
def device_state_attributes(self):
""" Returns state attributes. """
data = {
'about': "Weather forecast from yr.no, delivered by the"

View File

@ -111,11 +111,6 @@ class ZWaveSensor(ZWaveDeviceEntity, Entity):
""" Returns the state of the sensor. """
return self._value.data
@property
def state_attributes(self):
""" Returns optional state attributes. """
return self.device_state_attributes
@property
def unit_of_measurement(self):
return self._value.units

View File

@ -3,6 +3,9 @@ homeassistant.components.splunk
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Splunk component which allows you to send data to an Splunk instance
utilizing the HTTP Event Collector.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/splunk/
"""
import json
import logging

View File

@ -129,11 +129,6 @@ class SwitchDevice(ToggleEntity):
""" Is the device in standby. """
return None
@property
def device_state_attributes(self):
""" Returns device specific state attributes. """
return None
@property
def state_attributes(self):
""" Returns optional state attributes. """
@ -144,9 +139,4 @@ class SwitchDevice(ToggleEntity):
if value:
data[attr] = value
device_attr = self.device_state_attributes
if device_attr is not None:
data.update(device_attr)
return data

View File

@ -0,0 +1,103 @@
"""
homeassistant.components.switch.mfi
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Ubiquiti mFi switches.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.mfi/
"""
import logging
from homeassistant.components.switch import DOMAIN, SwitchDevice
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
REQUIREMENTS = ['mficlient==0.2.2']
_LOGGER = logging.getLogger(__name__)
SWITCH_MODELS = [
'Outlet',
'Output 5v',
'Output 12v',
'Output 24v',
]
# pylint: disable=unused-variable
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up mFi sensors. """
if not validate_config({DOMAIN: config},
{DOMAIN: ['host',
CONF_USERNAME,
CONF_PASSWORD]},
_LOGGER):
_LOGGER.error('A host, username, and password are required')
return False
host = config.get('host')
port = int(config.get('port', 6443))
username = config.get('username')
password = config.get('password')
from mficlient.client import MFiClient
try:
client = MFiClient(host, username, password, port=port)
except client.FailedToLogin as ex:
_LOGGER.error('Unable to connect to mFi: %s', str(ex))
return False
add_devices(MfiSwitch(port)
for device in client.get_devices()
for port in device.ports.values()
if port.model in SWITCH_MODELS)
class MfiSwitch(SwitchDevice):
""" An mFi switch-able device. """
def __init__(self, port):
self._port = port
self._target_state = None
@property
def should_poll(self):
return True
@property
def unique_id(self):
return self._port.ident
@property
def name(self):
return self._port.label
@property
def is_on(self):
return self._port.output
def update(self):
self._port.refresh()
if self._target_state is not None:
self._port.data['output'] = float(self._target_state)
self._target_state = None
def turn_on(self):
self._port.control(True)
self._target_state = True
def turn_off(self):
self._port.control(False)
self._target_state = False
@property
def current_power_mwh(self):
return int(self._port.data.get('active_pwr', 0) * 1000)
@property
def device_state_attributes(self):
attr = {}
attr['volts'] = round(self._port.data.get('v_rms', 0), 1)
attr['amps'] = round(self._port.data.get('i_rms', 0), 1)
return attr

View File

@ -100,28 +100,24 @@ class MySensorsSwitch(SwitchDevice):
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
device_attr = {}
for value_type, value in self._values.items():
if value_type != self.value_type:
device_attr[self.gateway.const.SetReq(value_type).name] = value
return device_attr
@property
def state_attributes(self):
"""Return the state attributes."""
data = {
attr = {
mysensors.ATTR_PORT: self.gateway.port,
mysensors.ATTR_NODE_ID: self.node_id,
mysensors.ATTR_CHILD_ID: self.child_id,
ATTR_BATTERY_LEVEL: self.battery_level,
}
device_attr = self.device_state_attributes
set_req = self.gateway.const.SetReq
if device_attr is not None:
data.update(device_attr)
return data
for value_type, value in self._values.items():
if value_type != self.value_type:
try:
attr[set_req(value_type).name] = value
except ValueError:
_LOGGER.error('value_type %s is not valid for mysensors '
'version %s', value_type,
self.gateway.version)
return attr
@property
def is_on(self):
@ -144,6 +140,11 @@ class MySensorsSwitch(SwitchDevice):
self._values[self.value_type] = STATE_OFF
self.update_ha_state()
@property
def available(self):
"""Return True if entity is available."""
return self.value_type in self._values
def update(self):
"""Update the controller with the latest value from a sensor."""
node = self.gateway.sensors[self.node_id]

View File

@ -8,8 +8,7 @@ https://home-assistant.io/components/switch.tellstick/
"""
import logging
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
ATTR_FRIENDLY_NAME)
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import ToggleEntity
SIGNAL_REPETITIONS = 1
@ -63,7 +62,6 @@ class TellstickSwitchDevice(ToggleEntity):
import tellcore.constants as tellcore_constants
self.tellstick_device = tellstick_device
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
self.signal_repetitions = signal_repetitions
self.last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
@ -79,11 +77,6 @@ class TellstickSwitchDevice(ToggleEntity):
""" Returns the name of the switch if any. """
return self.tellstick_device.name
@property
def state_attributes(self):
""" Returns optional state attributes. """
return self.state_attr
@property
def is_on(self):
""" True if switch is on. """

View File

@ -0,0 +1,162 @@
"""
homeassistant.components.switch.template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Allows the creation of a switch that integrates other components together
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.template/
"""
import logging
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.components.switch import SwitchDevice
from homeassistant.core import EVENT_STATE_CHANGED
from homeassistant.const import (
STATE_ON,
STATE_OFF,
ATTR_FRIENDLY_NAME,
CONF_VALUE_TEMPLATE)
from homeassistant.helpers.service import call_from_config
from homeassistant.util import template, slugify
from homeassistant.exceptions import TemplateError
from homeassistant.components.switch import DOMAIN
ENTITY_ID_FORMAT = DOMAIN + '.{}'
_LOGGER = logging.getLogger(__name__)
CONF_SWITCHES = 'switches'
STATE_ERROR = 'error'
ON_ACTION = 'turn_on'
OFF_ACTION = 'turn_off'
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the switches. """
switches = []
if config.get(CONF_SWITCHES) is None:
_LOGGER.error("Missing configuration data for switch platform")
return False
for device, device_config in config[CONF_SWITCHES].items():
if device != slugify(device):
_LOGGER.error("Found invalid key for switch.template: %s. "
"Use %s instead", device, slugify(device))
continue
if not isinstance(device_config, dict):
_LOGGER.error("Missing configuration data for switch %s", device)
continue
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
state_template = device_config.get(CONF_VALUE_TEMPLATE)
on_action = device_config.get(ON_ACTION)
off_action = device_config.get(OFF_ACTION)
if state_template is None:
_LOGGER.error(
"Missing %s for switch %s", CONF_VALUE_TEMPLATE, device)
continue
if on_action is None or off_action is None:
_LOGGER.error(
"Missing action for switch %s", device)
continue
switches.append(
SwitchTemplate(
hass,
device,
friendly_name,
state_template,
on_action,
off_action)
)
if not switches:
_LOGGER.error("No switches added")
return False
add_devices(switches)
return True
class SwitchTemplate(SwitchDevice):
""" Represents a Template Switch. """
# pylint: disable=too-many-arguments
def __init__(self,
hass,
device_id,
friendly_name,
state_template,
on_action,
off_action):
self.entity_id = generate_entity_id(
ENTITY_ID_FORMAT, device_id,
hass=hass)
self.hass = hass
self._name = friendly_name
self._template = state_template
self._on_action = on_action
self._off_action = off_action
self.update()
def _update_callback(_event):
""" Called when the target device changes state. """
self.update_ha_state(True)
self.hass.bus.listen(EVENT_STATE_CHANGED, _update_callback)
@property
def name(self):
""" Returns the name of the device. """
return self._name
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False
def turn_on(self, **kwargs):
""" Fires the on action. """
call_from_config(self.hass, self._on_action, True)
def turn_off(self, **kwargs):
""" Fires the off action. """
call_from_config(self.hass, self._off_action, True)
@property
def is_on(self):
""" True if device is on. """
return self._value.lower() == 'true' or self._value == STATE_ON
@property
def is_off(self):
""" True if device is off. """
return self._value.lower() == 'false' or self._value == STATE_OFF
@property
def available(self):
"""Return True if entity is available."""
return self.is_on or self.is_off
def update(self):
""" Updates the state from the template. """
try:
self._value = template.render(self.hass, self._template)
if not self.available:
_LOGGER.error(
"`%s` is not a switch state, setting %s to unavailable",
self._value, self.entity_id)
except TemplateError as ex:
self._value = STATE_ERROR
_LOGGER.error(ex)

View File

@ -21,7 +21,7 @@ from homeassistant.const import (
STATE_ON,
STATE_OFF)
REQUIREMENTS = ['pyvera==0.2.7']
REQUIREMENTS = ['pyvera==0.2.8']
_LOGGER = logging.getLogger(__name__)
@ -102,8 +102,8 @@ class VeraSwitch(SwitchDevice):
return self._name
@property
def state_attributes(self):
attr = super().state_attributes or {}
def device_state_attributes(self):
attr = {}
if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'

View File

@ -12,7 +12,7 @@ from homeassistant.components.switch import SwitchDevice
from homeassistant.const import (
STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = ['pywemo==0.3.8']
REQUIREMENTS = ['pywemo==0.3.9']
_LOGGER = logging.getLogger(__name__)
_WEMO_SUBSCRIPTION_REGISTRY = None
@ -95,8 +95,8 @@ class WemoSwitch(SwitchDevice):
return self.wemo.name
@property
def state_attributes(self):
attr = super().state_attributes or {}
def device_state_attributes(self):
attr = {}
if self.maker_params:
# Is the maker sensor on or off.
if self.maker_params['hassensor']:
@ -153,6 +153,18 @@ class WemoSwitch(SwitchDevice):
""" True if switch is on. """
return self.wemo.get_state()
@property
def available(self):
""" True if switch is available. """
if (self.wemo.model_name == 'Insight' and
self.insight_params is None):
return False
if (self.wemo.model_name == 'Maker' and
self.maker_params is None):
return False
return True
def turn_on(self, **kwargs):
""" Turns the switch on. """
self.wemo.on()

View File

@ -11,7 +11,7 @@ import logging
from homeassistant.components.wink import WinkToggleDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.4.2']
REQUIREMENTS = ['python-wink==0.5.0']
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -178,11 +178,6 @@ class ThermostatDevice(Entity):
""" Returns the current state. """
return self.target_temperature or STATE_UNKNOWN
@property
def device_state_attributes(self):
""" Returns device specific state attributes. """
return None
@property
def state_attributes(self):
""" Returns optional state attributes. """
@ -211,11 +206,6 @@ class ThermostatDevice(Entity):
if is_fan_on is not None:
data[ATTR_FAN] = STATE_ON if is_fan_on else STATE_OFF
device_attr = self.device_state_attributes
if device_attr is not None:
data.update(device_attr)
return data
@property

View File

@ -9,33 +9,32 @@ https://home-assistant.io/components/thermostat.honeywell/
import logging
import socket
import requests
from homeassistant.components.thermostat import ThermostatDevice
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS)
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS,
TEMP_FAHRENHEIT)
REQUIREMENTS = ['evohomeclient==0.2.4']
_LOGGER = logging.getLogger(__name__)
CONF_AWAY_TEMP = "away_temperature"
US_SYSTEM_SWITCH_POSITIONS = {1: 'Heat',
2: 'Off',
3: 'Cool'}
US_BASEURL = 'https://mytotalconnectcomfort.com/portal'
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the honeywel thermostat. """
def _setup_round(username, password, config, add_devices):
from evohomeclient import EvohomeClient
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
try:
away_temp = float(config.get(CONF_AWAY_TEMP, 16))
except ValueError:
_LOGGER.error("value entered for item %s should convert to a number",
CONF_AWAY_TEMP)
return False
if username is None or password is None:
_LOGGER.error("Missing required configuration items %s or %s",
CONF_USERNAME, CONF_PASSWORD)
return False
evo_api = EvohomeClient(username, password)
@ -53,6 +52,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the honeywel thermostat. """
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
thermostat_id = config.get('id')
if username is None or password is None:
_LOGGER.error("Missing required configuration items %s or %s",
CONF_USERNAME, CONF_PASSWORD)
return False
if thermostat_id:
add_devices([HoneywellUSThermostat(thermostat_id, username, password)])
else:
return _setup_round(username, password, config, add_devices)
class RoundThermostat(ThermostatDevice):
""" Represents a Honeywell Round Connected thermostat. """
@ -135,3 +152,117 @@ class RoundThermostat(ThermostatDevice):
else:
self._name = data['name']
self._is_dhw = False
class HoneywellUSThermostat(ThermostatDevice):
""" Represents a Honeywell US Thermostat. """
def __init__(self, ident, username, password):
self._ident = ident
self._username = username
self._password = password
self._session = requests.Session()
# Maybe this should be configurable?
self._timeout = 30
# Yeah, really.
self._session.headers['X-Requested-With'] = 'XMLHttpRequest'
self._update()
def _login(self):
self._session.get(US_BASEURL, timeout=self._timeout)
params = {'UserName': self._username,
'Password': self._password,
'RememberMe': 'false',
'timeOffset': 480}
resp = self._session.post(US_BASEURL, params=params,
timeout=self._timeout)
if resp.status_code != 200:
_LOGGER('Login failed for user %(user)s',
dict(user=self._username))
return False
else:
return True
def _get_data(self):
if not self._login():
return
url = '%s/Device/CheckDataSession/%s' % (US_BASEURL, self._ident)
resp = self._session.get(url, timeout=self._timeout)
if resp.status_code < 300:
return resp.json()
else:
return {'error': resp.status_code}
def _set_data(self, data):
if not self._login():
return
url = '%s/Device/SubmitControlScreenChanges' % US_BASEURL
data['DeviceID'] = self._ident
resp = self._session.post(url, data=data, timeout=self._timeout)
if resp.status_code < 300:
return resp.json()
else:
return {'error': resp.status_code}
def _update(self):
self._data = self._get_data()['latestData']
@property
def is_fan_on(self):
return self._data['fanData']['fanIsRunning']
@property
def name(self):
return 'honeywell'
@property
def unit_of_measurement(self):
unit = self._data['uiData']['DisplayUnits']
if unit == 'F':
return TEMP_FAHRENHEIT
else:
return TEMP_CELCIUS
@property
def current_temperature(self):
self._update()
return self._data['uiData']['DispTemperature']
@property
def target_temperature(self):
setpoint = US_SYSTEM_SWITCH_POSITIONS.get(
self._data['uiData']['SystemSwitchPosition'],
'Off')
return self._data['uiData']['%sSetpoint' % setpoint]
def set_temperature(self, temperature):
""" Set target temperature. """
data = {'SystemSwitch': None,
'HeatSetpoint': None,
'CoolSetpoint': None,
'HeatNextPeriod': None,
'CoolNextPeriod': None,
'StatusHeat': None,
'StatusCool': None,
'FanMode': None}
setpoint = US_SYSTEM_SWITCH_POSITIONS.get(
self._data['uiData']['SystemSwitchPosition'],
'Off')
data['%sSetpoint' % setpoint] = temperature
self._set_data(data)
@property
def device_state_attributes(self):
""" Return device specific state attributes. """
fanmodes = {0: "auto",
1: "on",
2: "circulate"}
return {"fan": (self._data['fanData']['fanIsRunning'] and
'running' or 'idle'),
"fanmode": fanmodes[self._data['fanData']['fanMode']]}
def turn_away_mode_on(self):
pass
def turn_away_mode_off(self):
pass

View File

@ -26,6 +26,7 @@ DOMAIN = "verisure"
DISCOVER_SENSORS = 'verisure.sensors'
DISCOVER_SWITCHES = 'verisure.switches'
DISCOVER_ALARMS = 'verisure.alarm_control_panel'
DISCOVER_LOCKS = 'verisure.lock'
DEPENDENCIES = ['alarm_control_panel']
REQUIREMENTS = ['vsure==0.5.0']
@ -36,6 +37,7 @@ MY_PAGES = None
ALARM_STATUS = {}
SMARTPLUG_STATUS = {}
CLIMATE_STATUS = {}
LOCK_STATUS = {}
VERISURE_LOGIN_ERROR = None
VERISURE_ERROR = None
@ -44,6 +46,7 @@ SHOW_THERMOMETERS = True
SHOW_HYGROMETERS = True
SHOW_ALARM = True
SHOW_SMARTPLUGS = True
SHOW_LOCKS = True
CODE_DIGITS = 4
# if wrong password was given don't try again
@ -63,11 +66,12 @@ def setup(hass, config):
from verisure import MyPages, LoginError, Error
global SHOW_THERMOMETERS, SHOW_HYGROMETERS,\
SHOW_ALARM, SHOW_SMARTPLUGS, CODE_DIGITS
SHOW_ALARM, SHOW_SMARTPLUGS, SHOW_LOCKS, CODE_DIGITS
SHOW_THERMOMETERS = int(config[DOMAIN].get('thermometers', '1'))
SHOW_HYGROMETERS = int(config[DOMAIN].get('hygrometers', '1'))
SHOW_ALARM = int(config[DOMAIN].get('alarm', '1'))
SHOW_SMARTPLUGS = int(config[DOMAIN].get('smartplugs', '1'))
SHOW_LOCKS = int(config[DOMAIN].get('locks', '1'))
CODE_DIGITS = int(config[DOMAIN].get('code_digits', '4'))
global MY_PAGES
@ -87,11 +91,13 @@ def setup(hass, config):
update_alarm()
update_climate()
update_smartplug()
update_lock()
# Load components for the devices in the ISY controller that we support
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
('switch', DISCOVER_SWITCHES),
('alarm_control_panel', DISCOVER_ALARMS))):
('alarm_control_panel', DISCOVER_ALARMS),
('lock', DISCOVER_LOCKS))):
component = get_component(comp_name)
_LOGGER.info(config[DOMAIN])
bootstrap.setup_component(hass, component.DOMAIN, config)
@ -134,6 +140,11 @@ def update_smartplug():
update_component(MY_PAGES.smartplug.get, SMARTPLUG_STATUS)
def update_lock():
""" Updates the status of alarms. """
update_component(MY_PAGES.lock.get, LOCK_STATUS)
def update_component(get_function, status):
""" Updates the status of verisure components. """
if WRONG_PASSWORD_GIVEN:

View File

@ -13,10 +13,10 @@ from homeassistant.helpers import validate_config
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import (
EVENT_PLATFORM_DISCOVERED, CONF_ACCESS_TOKEN,
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME)
ATTR_SERVICE, ATTR_DISCOVERED)
DOMAIN = "wink"
REQUIREMENTS = ['python-wink==0.4.2']
REQUIREMENTS = ['python-wink==0.5.0']
DISCOVER_LIGHTS = "wink.lights"
DISCOVER_SWITCHES = "wink.switches"
@ -82,13 +82,6 @@ class WinkToggleDevice(ToggleEntity):
""" True if light is on. """
return self.wink.state()
@property
def state_attributes(self):
""" Returns optional state attributes. """
return {
ATTR_FRIENDLY_NAME: self.wink.name()
}
def turn_on(self, **kwargs):
""" Turns the switch on. """
self.wink.set_state(True)

View File

@ -200,3 +200,6 @@ HTTP_HEADER_EXPIRES = "Expires"
CONTENT_TYPE_JSON = "application/json"
CONTENT_TYPE_MULTIPART = 'multipart/x-mixed-replace; boundary={}'
CONTENT_TYPE_TEXT_PLAIN = 'text/plain'
# The exit code to send to request a restart
RESTART_EXIT_CODE = 100

View File

@ -10,9 +10,9 @@ import time
import logging
import signal
import threading
from types import MappingProxyType
import enum
import functools as ft
from collections import namedtuple
from homeassistant.const import (
__version__, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
@ -20,7 +20,8 @@ from homeassistant.const import (
EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL,
EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED,
TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME, ATTR_SERVICE_DATA)
TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME, ATTR_SERVICE_DATA,
RESTART_EXIT_CODE)
from homeassistant.exceptions import (
HomeAssistantError, InvalidEntityFormatError)
import homeassistant.util as util
@ -45,12 +46,6 @@ MIN_WORKER_THREAD = 2
_LOGGER = logging.getLogger(__name__)
# Temporary to support deprecated methods
_MockHA = namedtuple("MockHomeAssistant", ['bus'])
# The exit code to send to request a restart
RESTART_EXIT_CODE = 100
class HomeAssistant(object):
"""Root object of the Home Assistant home automation."""
@ -97,7 +92,10 @@ class HomeAssistant(object):
'Could not bind to SIGTERM. Are you running in a thread?')
while not request_shutdown.isSet():
time.sleep(1)
try:
time.sleep(1)
except KeyboardInterrupt:
break
self.stop()
return RESTART_EXIT_CODE if request_restart.isSet() else 0
@ -113,46 +111,6 @@ class HomeAssistant(object):
self.pool.stop()
def track_point_in_time(self, action, point_in_time):
"""Deprecated method as of 8/4/2015 to track point in time."""
_LOGGER.warning(
'hass.track_point_in_time is deprecated. '
'Please use homeassistant.helpers.event.track_point_in_time')
import homeassistant.helpers.event as helper
helper.track_point_in_time(self, action, point_in_time)
def track_point_in_utc_time(self, action, point_in_time):
"""Deprecated method as of 8/4/2015 to track point in UTC time."""
_LOGGER.warning(
'hass.track_point_in_utc_time is deprecated. '
'Please use homeassistant.helpers.event.track_point_in_utc_time')
import homeassistant.helpers.event as helper
helper.track_point_in_utc_time(self, action, point_in_time)
def track_utc_time_change(self, action,
year=None, month=None, day=None,
hour=None, minute=None, second=None):
"""Deprecated method as of 8/4/2015 to track UTC time change."""
# pylint: disable=too-many-arguments
_LOGGER.warning(
'hass.track_utc_time_change is deprecated. '
'Please use homeassistant.helpers.event.track_utc_time_change')
import homeassistant.helpers.event as helper
helper.track_utc_time_change(self, action, year, month, day, hour,
minute, second)
def track_time_change(self, action,
year=None, month=None, day=None,
hour=None, minute=None, second=None, utc=False):
"""Deprecated method as of 8/4/2015 to track time change."""
# pylint: disable=too-many-arguments
_LOGGER.warning(
'hass.track_time_change is deprecated. '
'Please use homeassistant.helpers.event.track_time_change')
import homeassistant.helpers.event as helper
helper.track_time_change(self, action, year, month, day, hour,
minute, second)
class JobPriority(util.OrderedEnum):
"""Provides job priorities for event bus jobs."""
@ -352,7 +310,7 @@ class State(object):
self.entity_id = entity_id.lower()
self.state = state
self.attributes = attributes or {}
self.attributes = MappingProxyType(attributes or {})
self.last_updated = dt_util.strip_microseconds(
last_updated or dt_util.utcnow())
@ -380,12 +338,6 @@ class State(object):
self.attributes.get(ATTR_FRIENDLY_NAME) or
self.object_id.replace('_', ' '))
def copy(self):
"""Return a copy of the state."""
return State(self.entity_id, self.state,
dict(self.attributes), self.last_changed,
self.last_updated)
def as_dict(self):
"""Return a dict representation of the State.
@ -394,7 +346,7 @@ class State(object):
"""
return {'entity_id': self.entity_id,
'state': self.state,
'attributes': self.attributes,
'attributes': dict(self.attributes),
'last_changed': dt_util.datetime_to_str(self.last_changed),
'last_updated': dt_util.datetime_to_str(self.last_updated)}
@ -458,14 +410,11 @@ class StateMachine(object):
def all(self):
"""Create a list of all states."""
with self._lock:
return [state.copy() for state in self._states.values()]
return list(self._states.values())
def get(self, entity_id):
"""Retrieve state of entity_id or None if not found."""
state = self._states.get(entity_id.lower())
# Make a copy so people won't mutate the state
return state.copy() if state else None
return self._states.get(entity_id.lower())
def is_state(self, entity_id, state):
"""Test if entity exists and is specified state."""
@ -526,15 +475,6 @@ class StateMachine(object):
self._bus.fire(EVENT_STATE_CHANGED, event_data)
def track_change(self, entity_ids, action, from_state=None, to_state=None):
"""DEPRECATED AS OF 8/4/2015."""
_LOGGER.warning(
'hass.states.track_change is deprecated. '
'Use homeassistant.helpers.event.track_state_change instead.')
import homeassistant.helpers.event as helper
helper.track_state_change(_MockHA(self._bus), entity_ids, action,
from_state, to_state)
# pylint: disable=too-few-public-methods
class Service(object):

View File

@ -80,7 +80,20 @@ class Entity(object):
@property
def state_attributes(self):
"""Return the state attributes."""
"""
Return the state attributes.
Implemented by component base class.
"""
return None
@property
def device_state_attributes(self):
"""
Return device specific state attributes.
Implemented by platform classes.
"""
return None
@property
@ -135,6 +148,11 @@ class Entity(object):
state = str(self.state)
attr = self.state_attributes or {}
device_attr = self.device_state_attributes
if device_attr is not None:
attr.update(device_attr)
if ATTR_UNIT_OF_MEASUREMENT not in attr and \
self.unit_of_measurement is not None:
attr[ATTR_UNIT_OF_MEASUREMENT] = str(self.unit_of_measurement)

View File

@ -85,7 +85,7 @@ def reproduce_state(hass, states, blocking=False):
# We group service calls for entities by service call
# json used to create a hashable version of dict with maybe lists in it
key = (service_domain, service,
json.dumps(state.attributes, sort_keys=True))
json.dumps(dict(state.attributes), sort_keys=True))
to_call[key].append(state.entity_id)
for (service_domain, service, service_data), entity_ids in to_call.items():

View File

@ -15,6 +15,7 @@ import socket
import random
import string
from functools import wraps
from types import MappingProxyType
from .dt import datetime_to_local_str, utcnow
@ -42,7 +43,7 @@ def slugify(text):
def repr_helper(inp):
""" Helps creating a more readable string representation of objects. """
if isinstance(inp, dict):
if isinstance(inp, (dict, MappingProxyType)):
return ", ".join(
repr_helper(key)+"="+repr_helper(item) for key, item
in inp.items())

View File

@ -48,7 +48,7 @@ def as_utc(dattim):
if dattim.tzinfo == UTC:
return dattim
elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE)
dattim = DEFAULT_TIME_ZONE.localize(dattim)
return dattim.astimezone(UTC)
@ -58,7 +58,7 @@ def as_local(dattim):
if dattim.tzinfo == DEFAULT_TIME_ZONE:
return dattim
elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=UTC)
dattim = UTC.localize(dattim)
return dattim.astimezone(DEFAULT_TIME_ZONE)

View File

@ -91,7 +91,7 @@ https://github.com/theolind/pymysensors/archive/005bff4c5ca7a56acd30e816bc3bcdb5
https://github.com/w1ll1am23/pygooglevoice-sms/archive/7c5ee9969b97a7992fc86a753fe9f20e3ffa3f7c.zip#pygooglevoice-sms==0.0.1
# homeassistant.components.influxdb
influxdb==2.11.0
influxdb==2.12.0
# homeassistant.components.insteon_hub
insteon_hub==0.4.5
@ -105,6 +105,10 @@ liffylights==0.9.4
# homeassistant.components.light.limitlessled
limitlessled==1.0.0
# homeassistant.components.sensor.mfi
# homeassistant.components.switch.mfi
mficlient==0.2.2
# homeassistant.components.discovery
netdisco==0.5.2
@ -153,6 +157,9 @@ pyicloud==0.7.2
# homeassistant.components.device_tracker.netgear
pynetgear==0.3.2
# homeassistant.components.alarm_control_panel.nx584
pynx584==0.1
# homeassistant.components.sensor.openweathermap
pyowm==2.3.0
@ -183,13 +190,16 @@ python-telegram-bot==3.2.0
# homeassistant.components.sensor.twitch
python-twitch==1.2.0
# homeassistant.components.garage_door.wink
python-wink==0.4.2
# homeassistant.components.wink
# homeassistant.components.garage_door.wink
# homeassistant.components.light.wink
# homeassistant.components.lock.wink
# homeassistant.components.sensor.wink
# homeassistant.components.switch.wink
python-wink==0.4.2
python-wink==0.5.0
# homeassistant.components.keyboard
pyuserinput==0.1.9
@ -197,10 +207,10 @@ pyuserinput==0.1.9
# homeassistant.components.light.vera
# homeassistant.components.sensor.vera
# homeassistant.components.switch.vera
pyvera==0.2.7
pyvera==0.2.8
# homeassistant.components.switch.wemo
pywemo==0.3.8
pywemo==0.3.9
# homeassistant.components.thermostat.radiotherm
radiotherm==1.2
@ -229,6 +239,9 @@ tellive-py==0.5.2
# homeassistant.components.switch.transmission
transmissionrpc==0.11
# homeassistant.components.camera.uvc
uvcclient==0.5
# homeassistant.components.verisure
vsure==0.5.0

View File

@ -1,6 +1,6 @@
#!/bin/sh
# script/test: Run test suite for application. Optionallly pass in a path to an
# script/test: Run test suite for application. Optionally pass in a path to an
# individual test file to run a single test.
cd "$(dirname "$0")/.."

View File

@ -0,0 +1,138 @@
"""
tests.components.sensor.test_mfi
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests mFi sensor.
"""
import unittest
import unittest.mock as mock
import homeassistant.core as ha
import homeassistant.components.sensor as sensor
import homeassistant.components.sensor.mfi as mfi
from homeassistant.const import TEMP_CELCIUS
class TestMfiSensorSetup(unittest.TestCase):
PLATFORM = mfi
COMPONENT = sensor
THING = 'sensor'
GOOD_CONFIG = {
'sensor': {
'platform': 'mfi',
'host': 'foo',
'port': 6123,
'username': 'user',
'password': 'pass',
}
}
def setup_method(self, method):
self.hass = ha.HomeAssistant()
self.hass.config.latitude = 32.87336
self.hass.config.longitude = 117.22743
def teardown_method(self, method):
""" Stop down stuff we started. """
self.hass.stop()
def test_setup_missing_config(self):
config = {
'sensor': {
'platform': 'mfi',
}
}
self.assertFalse(self.PLATFORM.setup_platform(self.hass, config, None))
@mock.patch('mficlient.client')
def test_setup_failed_login(self, mock_client):
mock_client.FailedToLogin = Exception()
mock_client.MFiClient.side_effect = mock_client.FailedToLogin
self.assertFalse(
self.PLATFORM.setup_platform(self.hass,
dict(self.GOOD_CONFIG),
None))
@mock.patch('mficlient.client.MFiClient')
def test_setup_minimum(self, mock_client):
config = dict(self.GOOD_CONFIG)
del config[self.THING]['port']
assert self.COMPONENT.setup(self.hass, config)
mock_client.assert_called_once_with('foo', 'user', 'pass',
port=6443)
@mock.patch('mficlient.client.MFiClient')
def test_setup_with_port(self, mock_client):
config = dict(self.GOOD_CONFIG)
config[self.THING]['port'] = 6123
assert self.COMPONENT.setup(self.hass, config)
mock_client.assert_called_once_with('foo', 'user', 'pass',
port=6123)
@mock.patch('mficlient.client.MFiClient')
@mock.patch('homeassistant.components.sensor.mfi.MfiSensor')
def test_setup_adds_proper_devices(self, mock_sensor, mock_client):
ports = {i: mock.MagicMock(model=model)
for i, model in enumerate(mfi.SENSOR_MODELS)}
ports['bad'] = mock.MagicMock(model='notasensor')
print(ports['bad'].model)
mock_client.return_value.get_devices.return_value = \
[mock.MagicMock(ports=ports)]
assert sensor.setup(self.hass, self.GOOD_CONFIG)
for ident, port in ports.items():
if ident != 'bad':
mock_sensor.assert_any_call(port, self.hass)
assert mock.call(ports['bad'], self.hass) not in mock_sensor.mock_calls
class TestMfiSensor(unittest.TestCase):
def setup_method(self, method):
self.hass = ha.HomeAssistant()
self.hass.config.latitude = 32.87336
self.hass.config.longitude = 117.22743
self.port = mock.MagicMock()
self.sensor = mfi.MfiSensor(self.port, self.hass)
def teardown_method(self, method):
""" Stop down stuff we started. """
self.hass.stop()
def test_name(self):
self.assertEqual(self.port.label, self.sensor.name)
def test_uom_temp(self):
self.port.tag = 'temperature'
self.assertEqual(TEMP_CELCIUS, self.sensor.unit_of_measurement)
def test_uom_power(self):
self.port.tag = 'active_pwr'
self.assertEqual('Watts', self.sensor.unit_of_measurement)
def test_uom_digital(self):
self.port.model = 'Input Digital'
self.assertEqual('State', self.sensor.unit_of_measurement)
def test_uom_unknown(self):
self.port.tag = 'balloons'
self.assertEqual('balloons', self.sensor.unit_of_measurement)
def test_state_digital(self):
self.port.model = 'Input Digital'
self.port.value = 0
self.assertEqual(mfi.STATE_OFF, self.sensor.state)
self.port.value = 1
self.assertEqual(mfi.STATE_ON, self.sensor.state)
self.port.value = 2
self.assertEqual(mfi.STATE_ON, self.sensor.state)
def test_state_digits(self):
self.port.tag = 'didyoucheckthedict?'
self.port.value = 1.25
with mock.patch.dict(mfi.DIGITS, {'didyoucheckthedict?': 1}):
self.assertEqual(1.2, self.sensor.state)
with mock.patch.dict(mfi.DIGITS, {}):
self.assertEqual(1.0, self.sensor.state)
def test_update(self):
self.sensor.update()
self.port.refresh.assert_called_once_with()

View File

@ -0,0 +1,98 @@
"""
tests.components.switch.test_mfi
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests mFi switch.
"""
import unittest
import unittest.mock as mock
import homeassistant.core as ha
import homeassistant.components.switch as switch
import homeassistant.components.switch.mfi as mfi
from tests.components.sensor import test_mfi as test_mfi_sensor
class TestMfiSwitchSetup(test_mfi_sensor.TestMfiSensorSetup):
PLATFORM = mfi
COMPONENT = switch
THING = 'switch'
GOOD_CONFIG = {
'switch': {
'platform': 'mfi',
'host': 'foo',
'port': 6123,
'username': 'user',
'password': 'pass',
}
}
@mock.patch('mficlient.client.MFiClient')
@mock.patch('homeassistant.components.switch.mfi.MfiSwitch')
def test_setup_adds_proper_devices(self, mock_switch, mock_client):
ports = {i: mock.MagicMock(model=model)
for i, model in enumerate(mfi.SWITCH_MODELS)}
ports['bad'] = mock.MagicMock(model='notaswitch')
print(ports['bad'].model)
mock_client.return_value.get_devices.return_value = \
[mock.MagicMock(ports=ports)]
assert self.COMPONENT.setup(self.hass, self.GOOD_CONFIG)
for ident, port in ports.items():
if ident != 'bad':
mock_switch.assert_any_call(port)
assert mock.call(ports['bad'], self.hass) not in mock_switch.mock_calls
class TestMfiSwitch(unittest.TestCase):
def setup_method(self, method):
self.hass = ha.HomeAssistant()
self.hass.config.latitude = 32.87336
self.hass.config.longitude = 117.22743
self.port = mock.MagicMock()
self.switch = mfi.MfiSwitch(self.port)
def teardown_method(self, method):
""" Stop down stuff we started. """
self.hass.stop()
def test_name(self):
self.assertEqual(self.port.label, self.switch.name)
def test_update(self):
self.switch.update()
self.port.refresh.assert_called_once_with()
def test_update_with_target_state(self):
self.switch._target_state = True
self.port.data = {}
self.port.data['output'] = 'stale'
self.switch.update()
self.assertEqual(1.0, self.port.data['output'])
self.assertEqual(None, self.switch._target_state)
self.port.data['output'] = 'untouched'
self.switch.update()
self.assertEqual('untouched', self.port.data['output'])
def test_turn_on(self):
self.switch.turn_on()
self.port.control.assert_called_once_with(True)
self.assertTrue(self.switch._target_state)
def test_turn_off(self):
self.switch.turn_off()
self.port.control.assert_called_once_with(False)
self.assertFalse(self.switch._target_state)
def test_current_power_mwh(self):
self.port.data = {'active_pwr': 1}
self.assertEqual(1000, self.switch.current_power_mwh)
def test_current_power_mwh_no_data(self):
self.port.data = {'notpower': 123}
self.assertEqual(0, self.switch.current_power_mwh)
def test_device_state_attributes(self):
self.port.data = {'v_rms': 1.25,
'i_rms': 2.75}
self.assertEqual({'volts': 1.2, 'amps': 2.8},
self.switch.device_state_attributes)

View File

@ -0,0 +1,311 @@
"""
tests.components.switch.template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests template switch.
"""
import homeassistant.core as ha
import homeassistant.components as core
import homeassistant.components.switch as switch
from homeassistant.const import (
STATE_ON,
STATE_OFF)
class TestTemplateSwitch:
""" Test the Template switch. """
def setup_method(self, method):
self.hass = ha.HomeAssistant()
self.calls = []
def record_call(service):
self.calls.append(service)
self.hass.services.register('test', 'automation', record_call)
def teardown_method(self, method):
""" Stop down stuff we started. """
self.hass.stop()
def test_template_state_text(self):
assert switch.setup(self.hass, {
'switch': {
'platform': 'template',
'switches': {
'test_template_switch': {
'value_template':
"{{ states.switch.test_state.state }}",
'turn_on': {
'service': 'switch.turn_on',
'entity_id': 'switch.test_state'
},
'turn_off': {
'service': 'switch.turn_off',
'entity_id': 'switch.test_state'
},
}
}
}
})
state = self.hass.states.set('switch.test_state', STATE_ON)
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test_template_switch')
assert state.state == STATE_ON
state = self.hass.states.set('switch.test_state', STATE_OFF)
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test_template_switch')
assert state.state == STATE_OFF
def test_template_state_boolean_on(self):
assert switch.setup(self.hass, {
'switch': {
'platform': 'template',
'switches': {
'test_template_switch': {
'value_template':
"{{ 1 == 1 }}",
'turn_on': {
'service': 'switch.turn_on',
'entity_id': 'switch.test_state'
},
'turn_off': {
'service': 'switch.turn_off',
'entity_id': 'switch.test_state'
},
}
}
}
})
state = self.hass.states.get('switch.test_template_switch')
assert state.state == STATE_ON
def test_template_state_boolean_off(self):
assert switch.setup(self.hass, {
'switch': {
'platform': 'template',
'switches': {
'test_template_switch': {
'value_template':
"{{ 1 == 2 }}",
'turn_on': {
'service': 'switch.turn_on',
'entity_id': 'switch.test_state'
},
'turn_off': {
'service': 'switch.turn_off',
'entity_id': 'switch.test_state'
},
}
}
}
})
state = self.hass.states.get('switch.test_template_switch')
assert state.state == STATE_OFF
def test_template_syntax_error(self):
assert switch.setup(self.hass, {
'switch': {
'platform': 'template',
'switches': {
'test_template_switch': {
'value_template':
"{% if rubbish %}",
'turn_on': {
'service': 'switch.turn_on',
'entity_id': 'switch.test_state'
},
'turn_off': {
'service': 'switch.turn_off',
'entity_id': 'switch.test_state'
},
}
}
}
})
state = self.hass.states.set('switch.test_state', STATE_ON)
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test_template_switch')
assert state.state == 'unavailable'
def test_invalid_name_does_not_create(self):
assert switch.setup(self.hass, {
'switch': {
'platform': 'template',
'switches': {
'test INVALID switch': {
'value_template':
"{{ rubbish }",
'turn_on': {
'service': 'switch.turn_on',
'entity_id': 'switch.test_state'
},
'turn_off': {
'service': 'switch.turn_off',
'entity_id': 'switch.test_state'
},
}
}
}
})
assert self.hass.states.all() == []
def test_invalid_switch_does_not_create(self):
assert switch.setup(self.hass, {
'switch': {
'platform': 'template',
'switches': {
'test_template_switch': 'Invalid'
}
}
})
assert self.hass.states.all() == []
def test_no_switches_does_not_create(self):
assert switch.setup(self.hass, {
'switch': {
'platform': 'template'
}
})
assert self.hass.states.all() == []
def test_missing_template_does_not_create(self):
assert switch.setup(self.hass, {
'switch': {
'platform': 'template',
'switches': {
'test_template_switch': {
'not_value_template':
"{{ states.switch.test_state.state }}",
'turn_on': {
'service': 'switch.turn_on',
'entity_id': 'switch.test_state'
},
'turn_off': {
'service': 'switch.turn_off',
'entity_id': 'switch.test_state'
},
}
}
}
})
assert self.hass.states.all() == []
def test_missing_on_does_not_create(self):
assert switch.setup(self.hass, {
'switch': {
'platform': 'template',
'switches': {
'test_template_switch': {
'value_template':
"{{ states.switch.test_state.state }}",
'not_on': {
'service': 'switch.turn_on',
'entity_id': 'switch.test_state'
},
'turn_off': {
'service': 'switch.turn_off',
'entity_id': 'switch.test_state'
},
}
}
}
})
assert self.hass.states.all() == []
def test_missing_off_does_not_create(self):
assert switch.setup(self.hass, {
'switch': {
'platform': 'template',
'switches': {
'test_template_switch': {
'value_template':
"{{ states.switch.test_state.state }}",
'turn_on': {
'service': 'switch.turn_on',
'entity_id': 'switch.test_state'
},
'not_off': {
'service': 'switch.turn_off',
'entity_id': 'switch.test_state'
},
}
}
}
})
assert self.hass.states.all() == []
def test_on_action(self):
assert switch.setup(self.hass, {
'switch': {
'platform': 'template',
'switches': {
'test_template_switch': {
'value_template':
"{{ states.switch.test_state.state }}",
'turn_on': {
'service': 'test.automation'
},
'turn_off': {
'service': 'switch.turn_off',
'entity_id': 'switch.test_state'
},
}
}
}
})
self.hass.states.set('switch.test_state', STATE_OFF)
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test_template_switch')
assert state.state == STATE_OFF
core.switch.turn_on(self.hass, 'switch.test_template_switch')
self.hass.pool.block_till_done()
assert 1 == len(self.calls)
def test_off_action(self):
assert switch.setup(self.hass, {
'switch': {
'platform': 'template',
'switches': {
'test_template_switch': {
'value_template':
"{{ states.switch.test_state.state }}",
'turn_on': {
'service': 'switch.turn_on',
'entity_id': 'switch.test_state'
},
'turn_off': {
'service': 'test.automation'
},
}
}
}
})
self.hass.states.set('switch.test_state', STATE_ON)
self.hass.pool.block_till_done()
state = self.hass.states.get('switch.test_template_switch')
assert state.state == STATE_ON
core.switch.turn_off(self.hass, 'switch.test_template_switch')
self.hass.pool.block_till_done()
assert 1 == len(self.calls)

View File

@ -236,3 +236,14 @@ class TestComponentsGroup(unittest.TestCase):
grp2 = group.Group(self.hass, 'Je suis Charlie')
self.assertNotEqual(grp1.entity_id, grp2.entity_id)
def test_expand_entity_ids_expands_nested_groups(self):
group.Group(self.hass, 'light', ['light.test_1', 'light.test_2'])
group.Group(self.hass, 'switch', ['switch.test_1', 'switch.test_2'])
group.Group(self.hass, 'group_of_groups', ['group.light',
'group.switch'])
self.assertEqual(
['light.test_1', 'light.test_2', 'switch.test_1', 'switch.test_2'],
sorted(group.expand_entity_ids(self.hass,
['group.group_of_groups'])))

View File

@ -68,7 +68,7 @@ class TestMQTT(unittest.TestCase):
self.assertEqual('test-payload',
self.calls[0][0].data['service_data'][mqtt.ATTR_PAYLOAD])
def test_service_call_without_topic_does_not_publush(self):
def test_service_call_without_topic_does_not_publish(self):
self.hass.bus.fire(EVENT_CALL_SERVICE, {
ATTR_DOMAIN: mqtt.DOMAIN,
ATTR_SERVICE: mqtt.SERVICE_PUBLISH
@ -76,6 +76,42 @@ class TestMQTT(unittest.TestCase):
self.hass.pool.block_till_done()
self.assertTrue(not mqtt.MQTT_CLIENT.publish.called)
def test_service_call_with_template_payload_renders_template(self):
"""
If 'payload_template' is provided and 'payload' is not, then render it.
"""
mqtt.publish_template(self.hass, "test/topic", "{{ 1+1 }}")
self.hass.pool.block_till_done()
self.assertTrue(mqtt.MQTT_CLIENT.publish.called)
self.assertEqual(mqtt.MQTT_CLIENT.publish.call_args[0][1], "2")
def test_service_call_with_payload_doesnt_render_template(self):
"""
If a 'payload' is provided then use that instead of 'payload_template'.
"""
payload = "not a template"
payload_template = "a template"
# Call the service directly because the helper functions don't allow
# you to provide payload AND payload_template.
self.hass.services.call(mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, {
mqtt.ATTR_TOPIC: "test/topic",
mqtt.ATTR_PAYLOAD: payload,
mqtt.ATTR_PAYLOAD_TEMPLATE: payload_template
}, blocking=True)
self.assertTrue(mqtt.MQTT_CLIENT.publish.called)
self.assertEqual(mqtt.MQTT_CLIENT.publish.call_args[0][1], payload)
def test_service_call_without_payload_or_payload_template(self):
"""
If neither 'payload' or 'payload_template' is provided then fail.
"""
# Call the service directly because the helper functions require you to
# provide a payload.
self.hass.services.call(mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, {
mqtt.ATTR_TOPIC: "test/topic"
}, blocking=True)
self.assertFalse(mqtt.MQTT_CLIENT.publish.called)
def test_subscribe_topic(self):
mqtt.subscribe(self.hass, 'test-topic', self.record_calls)

View File

@ -0,0 +1,616 @@
"""
tests.components.proximity
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests proximity component.
"""
import homeassistant.core as ha
from homeassistant.components import proximity
class TestProximity:
""" Test the Proximity component. """
def setup_method(self, method):
self.hass = ha.HomeAssistant()
self.hass.states.set(
'zone.home', 'zoning',
{
'name': 'home',
'latitude': 2.1,
'longitude': 1.1,
'radius': 10
})
def teardown_method(self, method):
""" Stop down stuff we started. """
self.hass.stop()
def test_proximity(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
},
'tolerance': '1'
}
})
state = self.hass.states.get('proximity.home')
assert state.state == 'not set'
assert state.attributes.get('nearest') == 'not set'
assert state.attributes.get('dir_of_travel') == 'not set'
self.hass.states.set('proximity.home', '0')
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.state == '0'
def test_no_devices_in_config(self):
assert not proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'tolerance': '1'
}
})
def test_no_tolerance_in_config(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
}
}
})
def test_no_ignored_zones_in_config(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'devices': {
'device_tracker.test1',
'device_tracker.test2'
},
'tolerance': '1'
}
})
def test_no_zone_in_config(self):
assert proximity.setup(self.hass, {
'proximity': {
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
},
'tolerance': '1'
}
})
def test_device_tracker_test1_in_zone(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1'
},
'tolerance': '1'
}
})
self.hass.states.set(
'device_tracker.test1', 'home',
{
'friendly_name': 'test1',
'latitude': 2.1,
'longitude': 1.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.state == '0'
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'arrived'
def test_device_trackers_in_zone(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
},
'tolerance': '1'
}
})
self.hass.states.set(
'device_tracker.test1', 'home',
{
'friendly_name': 'test1',
'latitude': 2.1,
'longitude': 1.1
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test2', 'home',
{
'friendly_name': 'test2',
'latitude': 2.1,
'longitude': 1.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.state == '0'
assert (state.attributes.get('nearest') == 'test1, test2') or (state.attributes.get('nearest') == 'test2, test1')
assert state.attributes.get('dir_of_travel') == 'arrived'
def test_device_tracker_test1_away(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1'
},
'tolerance': '1'
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
def test_device_tracker_test1_awayfurther(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1'
}
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 40.1,
'longitude': 20.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'away_from'
def test_device_tracker_test1_awaycloser(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1'
}
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 40.1,
'longitude': 20.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'towards'
def test_all_device_trackers_in_ignored_zone(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1'
}
}
})
self.hass.states.set(
'device_tracker.test1', 'work',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.state == 'not set'
assert state.attributes.get('nearest') == 'not set'
assert state.attributes.get('dir_of_travel') == 'not set'
def test_device_tracker_test1_no_coordinates(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1'
},
'tolerance': '1'
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'not set'
assert state.attributes.get('dir_of_travel') == 'not set'
def test_device_tracker_test1_awayfurther_than_test2_first_test1(self):
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2'
})
self.hass.pool.block_till_done()
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
}
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2',
'latitude': 40.1,
'longitude': 20.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
def test_device_tracker_test1_awayfurther_than_test2_first_test2(self):
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2'
})
self.hass.pool.block_till_done()
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
}
}
})
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2',
'latitude': 40.1,
'longitude': 20.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test2'
assert state.attributes.get('dir_of_travel') == 'unknown'
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
def test_device_tracker_test1_awayfurther_test2_in_ignored_zone(self):
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test2', 'work',
{
'friendly_name': 'test2'
})
self.hass.pool.block_till_done()
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
}
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
def test_device_tracker_test1_awayfurther_than_test2_first_test1_than_test2_than_test1(self):
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2'
})
self.hass.pool.block_till_done()
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
}
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 10.1,
'longitude': 5.1
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 40.1,
'longitude': 20.1
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 35.1,
'longitude': 15.1
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test1', 'work',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test2'
assert state.attributes.get('dir_of_travel') == 'unknown'
def test_device_tracker_test1_awayfurther_a_bit(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1'
},
'tolerance': 1000
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1000001,
'longitude': 10.1000001
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1000002,
'longitude': 10.1000002
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'stationary'
def test_device_tracker_test1_nearest_after_test2_in_ignored_zone(self):
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2'
})
self.hass.pool.block_till_done()
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
}
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2',
'latitude': 10.1,
'longitude': 5.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test2'
assert state.attributes.get('dir_of_travel') == 'unknown'
self.hass.states.set(
'device_tracker.test2', 'work',
{
'friendly_name': 'test2',
'latitude': 12.6,
'longitude': 7.6
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'

View File

@ -23,7 +23,7 @@ import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_state_change
from homeassistant.const import (
__version__, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
ATTR_FRIENDLY_NAME, TEMP_CELCIUS,
EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, TEMP_CELCIUS,
TEMP_FAHRENHEIT)
PST = pytz.timezone('America/Los_Angeles')
@ -93,65 +93,6 @@ class TestHomeAssistant(unittest.TestCase):
self.assertEqual(1, len(calls))
def test_track_point_in_time(self):
""" Test track point in time. """
before_birthday = datetime(1985, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC)
birthday_paulus = datetime(1986, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC)
after_birthday = datetime(1987, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC)
runs = []
self.hass.track_point_in_utc_time(
lambda x: runs.append(1), birthday_paulus)
self._send_time_changed(before_birthday)
self.hass.pool.block_till_done()
self.assertEqual(0, len(runs))
self._send_time_changed(birthday_paulus)
self.hass.pool.block_till_done()
self.assertEqual(1, len(runs))
# A point in time tracker will only fire once, this should do nothing
self._send_time_changed(birthday_paulus)
self.hass.pool.block_till_done()
self.assertEqual(1, len(runs))
self.hass.track_point_in_time(
lambda x: runs.append(1), birthday_paulus)
self._send_time_changed(after_birthday)
self.hass.pool.block_till_done()
self.assertEqual(2, len(runs))
def test_track_time_change(self):
""" Test tracking time change. """
wildcard_runs = []
specific_runs = []
self.hass.track_time_change(lambda x: wildcard_runs.append(1))
self.hass.track_utc_time_change(
lambda x: specific_runs.append(1), second=[0, 30])
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0))
self.hass.pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(1, len(wildcard_runs))
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15))
self.hass.pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(2, len(wildcard_runs))
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30))
self.hass.pool.block_till_done()
self.assertEqual(2, len(specific_runs))
self.assertEqual(3, len(wildcard_runs))
def _send_time_changed(self, now):
""" Send a time changed event. """
self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now})
class TestEvent(unittest.TestCase):
""" Test Event class. """
@ -267,18 +208,6 @@ class TestState(unittest.TestCase):
{ATTR_FRIENDLY_NAME: name})
self.assertEqual(name, state.name)
def test_copy(self):
state = ha.State('domain.hello', 'world', {'some': 'attr'})
# Patch dt_util.utcnow() so we know last_updated got copied too
with patch('homeassistant.core.dt_util.utcnow',
return_value=dt_util.utcnow() + timedelta(seconds=10)):
copy = state.copy()
self.assertEqual(state.entity_id, copy.entity_id)
self.assertEqual(state.state, copy.state)
self.assertEqual(state.attributes, copy.attributes)
self.assertEqual(state.last_changed, copy.last_changed)
self.assertEqual(state.last_updated, copy.last_updated)
def test_dict_conversion(self):
state = ha.State('domain.hello', 'world', {'some': 'attr'})
self.assertEqual(state, ha.State.from_dict(state.as_dict()))
@ -358,52 +287,11 @@ class TestStateMachine(unittest.TestCase):
# If it does not exist, we should get False
self.assertFalse(self.states.remove('light.Bowl'))
def test_track_change(self):
""" Test states.track_change. """
self.pool.add_worker()
# 2 lists to track how often our callbacks got called
specific_runs = []
wildcard_runs = []
self.states.track_change(
'light.Bowl', lambda a, b, c: specific_runs.append(1), 'on', 'off')
self.states.track_change(
'light.Bowl', lambda a, b, c: wildcard_runs.append(1),
ha.MATCH_ALL, ha.MATCH_ALL)
# Set same state should not trigger a state change/listener
self.states.set('light.Bowl', 'on')
self.bus._pool.block_till_done()
self.assertEqual(0, len(specific_runs))
self.assertEqual(0, len(wildcard_runs))
# State change off -> on
self.states.set('light.Bowl', 'off')
self.bus._pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(1, len(wildcard_runs))
# State change off -> off
self.states.set('light.Bowl', 'off', {"some_attr": 1})
self.bus._pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(2, len(wildcard_runs))
# State change off -> on
self.states.set('light.Bowl', 'on')
self.bus._pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(3, len(wildcard_runs))
def test_case_insensitivty(self):
self.pool.add_worker()
runs = []
track_state_change(
ha._MockHA(self.bus), 'light.BoWl', lambda a, b, c: runs.append(1),
ha.MATCH_ALL, ha.MATCH_ALL)
self.bus.listen(EVENT_STATE_CHANGED, lambda event: runs.append(event))
self.states.set('light.BOWL', 'off')
self.bus._pool.block_till_done()