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

This commit is contained in:
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 # omit pieces of code that rely on external devices being present
homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/arduino.py homeassistant/components/arduino.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
homeassistant/components/*/insteon_hub.py homeassistant/components/*/insteon_hub.py
@ -102,6 +106,7 @@ omit =
homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushetta.py homeassistant/components/notify/pushetta.py
homeassistant/components/notify/pushover.py homeassistant/components/notify/pushover.py
homeassistant/components/notify/rest.py
homeassistant/components/notify/slack.py homeassistant/components/notify/slack.py
homeassistant/components/notify/smtp.py homeassistant/components/notify/smtp.py
homeassistant/components/notify/syslog.py homeassistant/components/notify/syslog.py

View File

@ -7,10 +7,12 @@ import sys
import threading import threading
import os import os
import argparse import argparse
import time
from homeassistant import bootstrap from homeassistant import bootstrap
import homeassistant.config as config_util 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(): def validate_python():
@ -76,6 +78,11 @@ def get_arguments():
'--demo-mode', '--demo-mode',
action='store_true', action='store_true',
help='Start Home Assistant in demo mode') 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( parser.add_argument(
'--open-ui', '--open-ui',
action='store_true', action='store_true',
@ -207,8 +214,11 @@ def uninstall_osx():
print("Home Assistant has been uninstalled.") print("Home Assistant has been uninstalled.")
def setup_and_run_hass(config_dir, args): def setup_and_run_hass(config_dir, args, top_process=False):
""" Setup HASS and run. Block until stopped. """ """
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: if args.demo_mode:
config = { config = {
'frontend': {}, 'frontend': {},
@ -235,7 +245,11 @@ def setup_and_run_hass(config_dir, args):
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
hass.start() 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): def run_hass_process(hass_proc):
@ -243,8 +257,8 @@ def run_hass_process(hass_proc):
requested_stop = threading.Event() requested_stop = threading.Event()
hass_proc.daemon = True hass_proc.daemon = True
def request_stop(): def request_stop(*args):
""" request hass stop """ """ request hass stop, *args is for signal handler callback """
requested_stop.set() requested_stop.set()
hass_proc.terminate() hass_proc.terminate()
@ -262,7 +276,10 @@ def run_hass_process(hass_proc):
hass_proc.join() hass_proc.join()
except KeyboardInterrupt: except KeyboardInterrupt:
return False 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(): def main():
@ -277,14 +294,16 @@ def main():
# os x launchd functions # os x launchd functions
if args.install_osx: if args.install_osx:
install_osx() install_osx()
return return 0
if args.uninstall_osx: if args.uninstall_osx:
uninstall_osx() uninstall_osx()
return return 0
if args.restart_osx: if args.restart_osx:
uninstall_osx() uninstall_osx()
# A small delay is needed on some systems to let the unload finish.
time.sleep(0.5)
install_osx() install_osx()
return return 0
# daemon functions # daemon functions
if args.pid_file: if args.pid_file:
@ -294,12 +313,23 @@ def main():
if args.pid_file: if args.pid_file:
write_pid(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. # Run hass as child process. Restart if necessary.
keep_running = True keep_running = True
while keep_running: while keep_running:
hass_proc = Process(target=setup_and_run_hass, args=(config_dir, args)) 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__": if __name__ == "__main__":
main() sys.exit(main())

View File

@ -61,6 +61,8 @@ def setup(hass, config):
for alarm in target_alarms: for alarm in target_alarms:
getattr(alarm, method)(code) getattr(alarm, method)(code)
if alarm.should_poll:
alarm.update_ha_state(True)
descriptions = load_yaml_config_file( descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml')) 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 # Open another session to alarm.com to fire off the command
_alarm = Alarmdotcom(self._username, self._password, timeout=10) _alarm = Alarmdotcom(self._username, self._password, timeout=10)
_alarm.disarm() _alarm.disarm()
self.update_ha_state()
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
""" Send arm home command. """ """ Send arm home command. """
@ -100,7 +99,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
# Open another session to alarm.com to fire off the command # Open another session to alarm.com to fire off the command
_alarm = Alarmdotcom(self._username, self._password, timeout=10) _alarm = Alarmdotcom(self._username, self._password, timeout=10)
_alarm.arm_stay() _alarm.arm_stay()
self.update_ha_state()
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
""" Send arm away command. """ """ Send arm away command. """
@ -110,7 +108,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
# Open another session to alarm.com to fire off the command # Open another session to alarm.com to fire off the command
_alarm = Alarmdotcom(self._username, self._password, timeout=10) _alarm = Alarmdotcom(self._username, self._password, timeout=10)
_alarm.arm_away() _alarm.arm_away()
self.update_ha_state()
def _validate_code(self, code, state): def _validate_code(self, code, state):
""" Validate given code. """ """ 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' SERVICE_CAMERA = 'camera_service'
STATE_RECORDING = 'recording'
DEFAULT_RECORDING_SECONDS = 30 DEFAULT_RECORDING_SECONDS = 30
# Maps discovered services to their platforms # Maps discovered services to their platforms
@ -46,6 +44,7 @@ DIR_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S'
REC_DIR_PREFIX = 'recording-' REC_DIR_PREFIX = 'recording-'
REC_IMG_PREFIX = 'recording_image-' REC_IMG_PREFIX = 'recording_image-'
STATE_RECORDING = 'recording'
STATE_STREAMING = 'streaming' STATE_STREAMING = 'streaming'
STATE_IDLE = 'idle' STATE_IDLE = 'idle'
@ -121,33 +120,7 @@ def setup(hass, config):
try: try:
camera.is_streaming = True camera.is_streaming = True
camera.update_ha_state() camera.update_ha_state()
camera.mjpeg_stream(handler)
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)
except (requests.RequestException, IOError): except (requests.RequestException, IOError):
camera.is_streaming = False camera.is_streaming = False
@ -190,6 +163,34 @@ class Camera(Entity):
""" Return bytes of camera image. """ """ Return bytes of camera image. """
raise NotImplementedError() 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 @property
def state(self): def state(self):
""" Returns the state of the entity. """ """ 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.helpers import validate_config
from homeassistant.components.camera import DOMAIN, Camera from homeassistant.components.camera import DOMAIN, Camera
from homeassistant.const import HTTP_OK
CONTENT_TYPE_HEADER = 'Content-Type'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -41,6 +44,17 @@ class MjpegCamera(Camera):
self._password = device_info.get('password') self._password = device_info.get('password')
self._mjpeg_url = device_info['mjpeg_url'] 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): def camera_image(self):
""" Return a still image response from the camera. """ """ Return a still image response from the camera. """
@ -55,17 +69,23 @@ class MjpegCamera(Camera):
jpg = data[jpg_start:jpg_end + 2] jpg = data[jpg_start:jpg_end + 2]
return jpg return jpg
if self._username and self._password: with closing(self.camera_stream()) as response:
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) 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 @property
def name(self): def name(self):
""" Return the name of this device. """ """ Return the name of this device. """

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) state = self.hass.states.get(entity_id)
new_data = state.attributes new_data = dict(state.attributes)
new_data[ATTR_ERRORS] = error new_data[ATTR_ERRORS] = error
self.hass.states.set(entity_id, STATE_CONFIGURE, new_data) 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 """ """ 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)}([/*!*************************************!*\ !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 ***! !*** ./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 //# sourceMappingURL=service_worker.js.map

View File

@ -71,7 +71,7 @@ def expand_entity_ids(hass, entity_ids):
if domain == DOMAIN: if domain == DOMAIN:
found_ids.extend( found_ids.extend(
ent_id for ent_id 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) if ent_id not in found_ids)
else: else:

View File

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

View File

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

View File

@ -9,13 +9,20 @@ https://home-assistant.io/components/light.vera/
import logging import logging
from requests.exceptions import RequestException 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__) _LOGGER = logging.getLogger(__name__)
@ -67,17 +74,35 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
add_devices_callback(lights) add_devices_callback(lights)
class VeraLight(VeraSwitch): class VeraLight(Light):
""" Represents a Vera Light, including dimmable. """ """ 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 @property
def state_attributes(self): def name(self):
attr = super().state_attributes or {} """ Get the mame of the switch. """
return self._name
@property
def brightness(self):
"""Brightness of the light."""
if self.vera_device.is_dimmable: if self.vera_device.is_dimmable:
attr[ATTR_BRIGHTNESS] = self.vera_device.get_brightness() return self.vera_device.get_brightness()
return attr
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
if ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable: if ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable:
@ -87,3 +112,49 @@ class VeraLight(VeraSwitch):
self._state = STATE_ON self._state = STATE_ON
self.update_ha_state(True) 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 import logging
from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.light import ATTR_BRIGHTNESS, Light
from homeassistant.components.wink import WinkToggleDevice
from homeassistant.const import CONF_ACCESS_TOKEN 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): 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()) WinkLight(light) for light in pywink.get_bulbs())
class WinkLight(WinkToggleDevice): class WinkLight(Light):
""" Represents a Wink 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 # pylint: disable=too-few-public-methods
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
""" Turns the switch on. """ """ Turns the switch on. """
@ -48,14 +70,10 @@ class WinkLight(WinkToggleDevice):
else: else:
self.wink.set_state(True) self.wink.set_state(True)
@property def turn_off(self):
def state_attributes(self): """ Turns the switch off. """
attr = super().state_attributes self.wink.set_state(False)
if self.is_on: def update(self):
brightness = self.wink.brightness() """ Update state of the light. """
self.wink.update_state()
if brightness is not None:
attr[ATTR_BRIGHTNESS] = int(brightness * 255)
return attr

View File

@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.const import ( from homeassistant.const import (
STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK,
ATTR_ENTITY_ID) ATTR_ENTITY_ID)
from homeassistant.components import (group, wink) from homeassistant.components import (group, verisure, wink)
DOMAIN = 'lock' DOMAIN = 'lock'
SCAN_INTERVAL = 30 SCAN_INTERVAL = 30
@ -28,12 +28,15 @@ ENTITY_ID_ALL_LOCKS = group.ENTITY_ID_FORMAT.format('all_locks')
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
ATTR_LOCKED = "locked" ATTR_LOCKED = "locked"
ATTR_CODE = 'code'
ATTR_CODE_FORMAT = 'code_format'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
# Maps discovered services to their platforms # Maps discovered services to their platforms
DISCOVERY_PLATFORMS = { DISCOVERY_PLATFORMS = {
wink.DISCOVER_LOCKS: 'wink' wink.DISCOVER_LOCKS: 'wink',
verisure.DISCOVER_LOCKS: 'verisure'
} }
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -45,15 +48,25 @@ def is_locked(hass, entity_id=None):
return hass.states.is_state(entity_id, STATE_LOCKED) 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. """ """ 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) 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. """ """ 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) hass.services.call(DOMAIN, SERVICE_UNLOCK, data)
@ -68,11 +81,16 @@ def setup(hass, config):
""" Handles calls to the lock services. """ """ Handles calls to the lock services. """
target_locks = component.extract_from_service(service) 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: for item in target_locks:
if service.service == SERVICE_LOCK: if service.service == SERVICE_LOCK:
item.lock() item.lock(code=code)
else: else:
item.unlock() item.unlock(code=code)
if item.should_poll: if item.should_poll:
item.update_ha_state(True) item.update_ha_state(True)
@ -91,19 +109,34 @@ class LockDevice(Entity):
""" Represents a lock within Home Assistant. """ """ Represents a lock within Home Assistant. """
# pylint: disable=no-self-use # pylint: disable=no-self-use
@property
def code_format(self):
""" regex for code format or None if no code is required. """
return None
@property @property
def is_locked(self): def is_locked(self):
""" Is the lock locked or unlocked. """ """ Is the lock locked or unlocked. """
return None return None
def lock(self): def lock(self, **kwargs):
""" Locks the lock. """ """ Locks the lock. """
raise NotImplementedError() raise NotImplementedError()
def unlock(self): def unlock(self, **kwargs):
""" Unlocks the lock. """ """ Unlocks the lock. """
raise NotImplementedError() 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 @property
def state(self): def state(self):
locked = self.is_locked 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.components.lock import LockDevice
from homeassistant.const import CONF_ACCESS_TOKEN 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): 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. """ """ Flags of media commands that are supported. """
return 0 return 0
@property
def device_state_attributes(self):
""" Extra attributes a device wants to expose. """
return None
def turn_on(self): def turn_on(self):
""" turn the media player on. """ """ turn the media player on. """
raise NotImplementedError() raise NotImplementedError()
@ -546,9 +541,4 @@ class MediaPlayerDevice(Entity):
if self.media_image_url: if self.media_image_url:
state_attr[ATTR_ENTITY_PICTURE] = 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 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)] hosts = [(config[CONF_HOST], DEFAULT_PORT)]
else: else:
hosts = [host for host in pychromecast.discover_chromecasts() hosts = [tuple(dev[:2]) for dev in pychromecast.discover_chromecasts()
if host not in KNOWN_HOSTS] if tuple(dev[:2]) not in KNOWN_HOSTS]
casts = [] casts = []

View File

@ -12,8 +12,10 @@ import socket
import time import time
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
import homeassistant.util as util import homeassistant.util as util
from homeassistant.util import template
from homeassistant.helpers import validate_config from homeassistant.helpers import validate_config
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
@ -49,24 +51,34 @@ DEFAULT_PROTOCOL = PROTOCOL_311
ATTR_TOPIC = 'topic' ATTR_TOPIC = 'topic'
ATTR_PAYLOAD = 'payload' ATTR_PAYLOAD = 'payload'
ATTR_PAYLOAD_TEMPLATE = 'payload_template'
ATTR_QOS = 'qos' ATTR_QOS = 'qos'
ATTR_RETAIN = 'retain' ATTR_RETAIN = 'retain'
MAX_RECONNECT_WAIT = 300 # seconds MAX_RECONNECT_WAIT = 300 # seconds
def publish(hass, topic, payload, qos=None, retain=None): def _build_publish_data(topic, qos, retain):
"""Publish message to an MQTT topic.""" """Build the arguments for the publish service without the payload."""
data = { data = {ATTR_TOPIC: topic}
ATTR_TOPIC: topic,
ATTR_PAYLOAD: payload,
}
if qos is not None: if qos is not None:
data[ATTR_QOS] = qos data[ATTR_QOS] = qos
if retain is not None: if retain is not None:
data[ATTR_RETAIN] = retain 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) hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
@ -132,15 +144,34 @@ def setup(hass, config):
"""Handle MQTT publish service calls.""" """Handle MQTT publish service calls."""
msg_topic = call.data.get(ATTR_TOPIC) msg_topic = call.data.get(ATTR_TOPIC)
payload = call.data.get(ATTR_PAYLOAD) payload = call.data.get(ATTR_PAYLOAD)
payload_template = call.data.get(ATTR_PAYLOAD_TEMPLATE)
qos = call.data.get(ATTR_QOS, DEFAULT_QOS) qos = call.data.get(ATTR_QOS, DEFAULT_QOS)
retain = call.data.get(ATTR_RETAIN, DEFAULT_RETAIN) 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: if msg_topic is None or payload is None:
return return
MQTT_CLIENT.publish(msg_topic, payload, qos, retain) MQTT_CLIENT.publish(msg_topic, payload, qos, retain)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_mqtt) 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 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 = 'persistence'
CONF_PERSISTENCE_FILE = 'persistence_file' CONF_PERSISTENCE_FILE = 'persistence_file'
CONF_VERSION = 'version' CONF_VERSION = 'version'
CONF_BAUD_RATE = 'baud_rate'
DEFAULT_VERSION = '1.4' DEFAULT_VERSION = '1.4'
DEFAULT_BAUD_RATE = 115200
DOMAIN = 'mysensors' DOMAIN = 'mysensors'
DEPENDENCIES = [] DEPENDENCIES = []
@ -60,12 +62,13 @@ def setup(hass, config):
version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION)) version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION))
is_metric = (hass.config.temperature_unit == TEMP_CELCIUS) 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.""" """Return gateway after setup of the gateway."""
gateway = mysensors.SerialGateway(port, event_callback=None, gateway = mysensors.SerialGateway(port, event_callback=None,
persistence=persistence, persistence=persistence,
persistence_file=persistence_file, persistence_file=persistence_file,
protocol_version=version) protocol_version=version,
baud=baud_rate)
gateway.metric = is_metric gateway.metric = is_metric
gateway.debug = config[DOMAIN].get(CONF_DEBUG, False) gateway.debug = config[DOMAIN].get(CONF_DEBUG, False)
gateway = GatewayWrapper(gateway, version) gateway = GatewayWrapper(gateway, version)
@ -98,8 +101,9 @@ def setup(hass, config):
persistence_file = gway.get( persistence_file = gway.get(
CONF_PERSISTENCE_FILE, CONF_PERSISTENCE_FILE,
hass.config.path('mysensors{}.pickle'.format(index + 1))) hass.config.path('mysensors{}.pickle'.format(index + 1)))
baud_rate = gway.get(CONF_BAUD_RATE, DEFAULT_BAUD_RATE)
GATEWAYS[port] = setup_gateway( GATEWAYS[port] = setup_gateway(
port, persistence, persistence_file, version) port, persistence, persistence_file, version, baud_rate)
for (component, discovery_service) in DISCOVERY_COMPONENTS: for (component, discovery_service) in DISCOVERY_COMPONENTS:
# Ensure component is loaded # 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: else:
state_domain = state.domain state_domain = state.domain
state_state = state.state state_state = state.state
state_attr = json.dumps(state.attributes) state_attr = json.dumps(dict(state.attributes))
last_changed = state.last_changed last_changed = state.last_changed
last_updated = state.last_updated last_updated = state.last_updated
@ -285,7 +285,8 @@ class Recorder(threading.Thread):
else: else:
return cur.fetchall() return cur.fetchall()
except sqlite3.IntegrityError: except (sqlite3.IntegrityError, sqlite3.OperationalError,
sqlite3.ProgrammingError):
_LOGGER.exception( _LOGGER.exception(
"Error querying the database using: %s", sql_query) "Error querying the database using: %s", sql_query)
return [] return []
@ -329,7 +330,7 @@ class Recorder(threading.Thread):
migration_id = 0 migration_id = 0
if migration_id < 1: if migration_id < 1:
self.query(""" cur.execute("""
CREATE TABLE recorder_runs ( CREATE TABLE recorder_runs (
run_id integer primary key, run_id integer primary key,
start integer, start integer,
@ -338,7 +339,7 @@ class Recorder(threading.Thread):
created integer) created integer)
""") """)
self.query(""" cur.execute("""
CREATE TABLE events ( CREATE TABLE events (
event_id integer primary key, event_id integer primary key,
event_type text, event_type text,
@ -346,10 +347,10 @@ class Recorder(threading.Thread):
origin text, origin text,
created integer) created integer)
""") """)
self.query( cur.execute(
'CREATE INDEX events__event_type ON events(event_type)') 'CREATE INDEX events__event_type ON events(event_type)')
self.query(""" cur.execute("""
CREATE TABLE states ( CREATE TABLE states (
state_id integer primary key, state_id integer primary key,
entity_id text, entity_id text,
@ -359,51 +360,51 @@ class Recorder(threading.Thread):
last_updated integer, last_updated integer,
created 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) save_migration(1)
if migration_id < 2: if migration_id < 2:
self.query(""" cur.execute("""
ALTER TABLE events ALTER TABLE events
ADD COLUMN time_fired integer ADD COLUMN time_fired integer
""") """)
self.query('UPDATE events SET time_fired=created') cur.execute('UPDATE events SET time_fired=created')
save_migration(2) save_migration(2)
if migration_id < 3: if migration_id < 3:
utc_offset = self.utc_offset utc_offset = self.utc_offset
self.query(""" cur.execute("""
ALTER TABLE recorder_runs ALTER TABLE recorder_runs
ADD COLUMN utc_offset integer ADD COLUMN utc_offset integer
""") """)
self.query(""" cur.execute("""
ALTER TABLE events ALTER TABLE events
ADD COLUMN utc_offset integer ADD COLUMN utc_offset integer
""") """)
self.query(""" cur.execute("""
ALTER TABLE states ALTER TABLE states
ADD COLUMN utc_offset integer ADD COLUMN utc_offset integer
""") """)
self.query("UPDATE recorder_runs SET utc_offset=?", [utc_offset]) cur.execute("UPDATE recorder_runs SET utc_offset=?", [utc_offset])
self.query("UPDATE events SET utc_offset=?", [utc_offset]) cur.execute("UPDATE events SET utc_offset=?", [utc_offset])
self.query("UPDATE states SET utc_offset=?", [utc_offset]) cur.execute("UPDATE states SET utc_offset=?", [utc_offset])
save_migration(3) save_migration(3)
if migration_id < 4: if migration_id < 4:
# We had a bug where we did not save utc offset for recorder runs # We had a bug where we did not save utc offset for recorder runs
self.query( cur.execute(
"""UPDATE recorder_runs SET utc_offset=? """UPDATE recorder_runs SET utc_offset=?
WHERE utc_offset IS NULL""", [self.utc_offset]) WHERE utc_offset IS NULL""", [self.utc_offset])
self.query(""" cur.execute("""
ALTER TABLE states ALTER TABLE states
ADD COLUMN event_id integer ADD COLUMN event_id integer
""") """)
@ -412,25 +413,33 @@ class Recorder(threading.Thread):
if migration_id < 5: if migration_id < 5:
# Add domain so that thermostat graphs look right # Add domain so that thermostat graphs look right
self.query(""" try:
cur.execute("""
ALTER TABLE states ALTER TABLE states
ADD COLUMN domain text 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 # populate domain with defaults
rows = self.query("select distinct entity_id from states") cur.execute("""
for row in rows: UPDATE states
entity_id = row[0] set domain=substr(entity_id, 0, instr(entity_id, '.'))
domain = entity_id.split(".")[0] """)
self.query(
"UPDATE states set domain=? where entity_id=?",
domain, entity_id)
# add indexes we are going to use a lot on selects # add indexes we are going to use a lot on selects
self.query(""" cur.execute("""
CREATE INDEX states__state_changes ON CREATE INDEX states__state_changes ON
states (last_changed, last_updated, entity_id)""") states (last_changed, last_updated, entity_id)""")
self.query(""" cur.execute("""
CREATE INDEX states__significant_changes ON CREATE INDEX states__significant_changes ON
states (domain, last_updated, entity_id)""") states (domain, last_updated, entity_id)""")
save_migration(5) save_migration(5)

View File

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

View File

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

View File

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

View File

@ -63,6 +63,11 @@ class EcobeeSensor(Entity):
""" Returns the state of the device. """ """ Returns the state of the device. """
return self._state return self._state
@property
def unique_id(self):
"""Unique id of this sensor."""
return "sensor_ecobee_{}_{}".format(self.type, self.index)
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
""" Unit of measurement this sensor expresses itself in. """ """ 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/ https://home-assistant.io/components/sensor.mqtt/
""" """
import logging 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.helpers.entity import Entity
from homeassistant.util import template from homeassistant.util import template
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
@ -42,7 +42,7 @@ class MqttSensor(Entity):
""" Represents a sensor that can be updated using MQTT. """ """ Represents a sensor that can be updated using MQTT. """
def __init__(self, hass, name, state_topic, qos, unit_of_measurement, def __init__(self, hass, name, state_topic, qos, unit_of_measurement,
value_template): value_template):
self._state = "-" self._state = STATE_UNKNOWN
self._hass = hass self._hass = hass
self._name = name self._name = name
self._state_topic = state_topic 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_VOLTAGE: 'V',
self.gateway.const.SetReq.V_CURRENT: 'A', self.gateway.const.SetReq.V_CURRENT: 'A',
} }
unit_map_v15 = {
self.gateway.const.SetReq.V_PERCENTAGE: '%',
}
if float(self.gateway.version) >= 1.5: if float(self.gateway.version) >= 1.5:
if self.gateway.const.SetReq.V_UNIT_PREFIX in self._values: if self.gateway.const.SetReq.V_UNIT_PREFIX in self._values:
return self._values[ return self._values[
self.gateway.const.SetReq.V_UNIT_PREFIX] 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) return unit_map.get(self.value_type)
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return device specific state attributes.""" """Return device specific state attributes."""
device_attr = {} 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 = {
mysensors.ATTR_PORT: self.gateway.port, mysensors.ATTR_PORT: self.gateway.port,
mysensors.ATTR_NODE_ID: self.node_id, mysensors.ATTR_NODE_ID: self.node_id,
mysensors.ATTR_CHILD_ID: self.child_id, mysensors.ATTR_CHILD_ID: self.child_id,
ATTR_BATTERY_LEVEL: self.battery_level, ATTR_BATTERY_LEVEL: self.battery_level,
} }
device_attr = self.device_state_attributes set_req = self.gateway.const.SetReq
if device_attr is not None: for value_type, value in self._values.items():
data.update(device_attr) 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): def update(self):
"""Update the controller with the latest values from a sensor.""" """Update the controller with the latest values from a sensor."""

View File

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

View File

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

View File

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

View File

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

View File

@ -94,9 +94,6 @@ class SensorTemplate(Entity):
def _update_callback(_event): def _update_callback(_event):
""" Called when the target device changes state. """ """ 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) self.hass.bus.listen(EVENT_STATE_CHANGED, _update_callback)

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import logging
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED 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): def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -100,7 +100,7 @@ class YrSensor(Entity):
return self._state return self._state
@property @property
def state_attributes(self): def device_state_attributes(self):
""" Returns state attributes. """ """ Returns state attributes. """
data = { data = {
'about': "Weather forecast from yr.no, delivered by the" '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. """ """ Returns the state of the sensor. """
return self._value.data return self._value.data
@property
def state_attributes(self):
""" Returns optional state attributes. """
return self.device_state_attributes
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
return self._value.units 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 Splunk component which allows you to send data to an Splunk instance
utilizing the HTTP Event Collector. 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 json
import logging import logging

View File

@ -129,11 +129,6 @@ class SwitchDevice(ToggleEntity):
""" Is the device in standby. """ """ Is the device in standby. """
return None return None
@property
def device_state_attributes(self):
""" Returns device specific state attributes. """
return None
@property @property
def state_attributes(self): def state_attributes(self):
""" Returns optional state attributes. """ """ Returns optional state attributes. """
@ -144,9 +139,4 @@ class SwitchDevice(ToggleEntity):
if value: if value:
data[attr] = value data[attr] = value
device_attr = self.device_state_attributes
if device_attr is not None:
data.update(device_attr)
return data 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 @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return device specific state attributes.""" """Return device specific state attributes."""
device_attr = {} 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 = {
mysensors.ATTR_PORT: self.gateway.port, mysensors.ATTR_PORT: self.gateway.port,
mysensors.ATTR_NODE_ID: self.node_id, mysensors.ATTR_NODE_ID: self.node_id,
mysensors.ATTR_CHILD_ID: self.child_id, mysensors.ATTR_CHILD_ID: self.child_id,
ATTR_BATTERY_LEVEL: self.battery_level, ATTR_BATTERY_LEVEL: self.battery_level,
} }
device_attr = self.device_state_attributes set_req = self.gateway.const.SetReq
if device_attr is not None: for value_type, value in self._values.items():
data.update(device_attr) if value_type != self.value_type:
try:
return data 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 @property
def is_on(self): def is_on(self):
@ -144,6 +140,11 @@ class MySensorsSwitch(SwitchDevice):
self._values[self.value_type] = STATE_OFF self._values[self.value_type] = STATE_OFF
self.update_ha_state() self.update_ha_state()
@property
def available(self):
"""Return True if entity is available."""
return self.value_type in self._values
def update(self): def update(self):
"""Update the controller with the latest value from a sensor.""" """Update the controller with the latest value from a sensor."""
node = self.gateway.sensors[self.node_id] node = self.gateway.sensors[self.node_id]

View File

@ -8,8 +8,7 @@ https://home-assistant.io/components/switch.tellstick/
""" """
import logging import logging
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, from homeassistant.const import EVENT_HOMEASSISTANT_STOP
ATTR_FRIENDLY_NAME)
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
SIGNAL_REPETITIONS = 1 SIGNAL_REPETITIONS = 1
@ -63,7 +62,6 @@ class TellstickSwitchDevice(ToggleEntity):
import tellcore.constants as tellcore_constants import tellcore.constants as tellcore_constants
self.tellstick_device = tellstick_device self.tellstick_device = tellstick_device
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
self.signal_repetitions = signal_repetitions self.signal_repetitions = signal_repetitions
self.last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | self.last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
@ -79,11 +77,6 @@ class TellstickSwitchDevice(ToggleEntity):
""" Returns the name of the switch if any. """ """ Returns the name of the switch if any. """
return self.tellstick_device.name return self.tellstick_device.name
@property
def state_attributes(self):
""" Returns optional state attributes. """
return self.state_attr
@property @property
def is_on(self): def is_on(self):
""" True if switch is on. """ """ 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_ON,
STATE_OFF) STATE_OFF)
REQUIREMENTS = ['pyvera==0.2.7'] REQUIREMENTS = ['pyvera==0.2.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -102,8 +102,8 @@ class VeraSwitch(SwitchDevice):
return self._name return self._name
@property @property
def state_attributes(self): def device_state_attributes(self):
attr = super().state_attributes or {} attr = {}
if self.vera_device.has_battery: if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'

View File

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

View File

@ -11,7 +11,7 @@ import logging
from homeassistant.components.wink import WinkToggleDevice from homeassistant.components.wink import WinkToggleDevice
from homeassistant.const import CONF_ACCESS_TOKEN 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): def setup_platform(hass, config, add_devices, discovery_info=None):

View File

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

View File

@ -9,33 +9,32 @@ https://home-assistant.io/components/thermostat.honeywell/
import logging import logging
import socket import socket
import requests
from homeassistant.components.thermostat import ThermostatDevice 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'] REQUIREMENTS = ['evohomeclient==0.2.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_AWAY_TEMP = "away_temperature" 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_round(username, password, config, add_devices):
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the honeywel thermostat. """
from evohomeclient import EvohomeClient from evohomeclient import EvohomeClient
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
try: try:
away_temp = float(config.get(CONF_AWAY_TEMP, 16)) away_temp = float(config.get(CONF_AWAY_TEMP, 16))
except ValueError: except ValueError:
_LOGGER.error("value entered for item %s should convert to a number", _LOGGER.error("value entered for item %s should convert to a number",
CONF_AWAY_TEMP) CONF_AWAY_TEMP)
return False 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) evo_api = EvohomeClient(username, password)
@ -53,6 +52,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False 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): class RoundThermostat(ThermostatDevice):
""" Represents a Honeywell Round Connected thermostat. """ """ Represents a Honeywell Round Connected thermostat. """
@ -135,3 +152,117 @@ class RoundThermostat(ThermostatDevice):
else: else:
self._name = data['name'] self._name = data['name']
self._is_dhw = False 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_SENSORS = 'verisure.sensors'
DISCOVER_SWITCHES = 'verisure.switches' DISCOVER_SWITCHES = 'verisure.switches'
DISCOVER_ALARMS = 'verisure.alarm_control_panel' DISCOVER_ALARMS = 'verisure.alarm_control_panel'
DISCOVER_LOCKS = 'verisure.lock'
DEPENDENCIES = ['alarm_control_panel'] DEPENDENCIES = ['alarm_control_panel']
REQUIREMENTS = ['vsure==0.5.0'] REQUIREMENTS = ['vsure==0.5.0']
@ -36,6 +37,7 @@ MY_PAGES = None
ALARM_STATUS = {} ALARM_STATUS = {}
SMARTPLUG_STATUS = {} SMARTPLUG_STATUS = {}
CLIMATE_STATUS = {} CLIMATE_STATUS = {}
LOCK_STATUS = {}
VERISURE_LOGIN_ERROR = None VERISURE_LOGIN_ERROR = None
VERISURE_ERROR = None VERISURE_ERROR = None
@ -44,6 +46,7 @@ SHOW_THERMOMETERS = True
SHOW_HYGROMETERS = True SHOW_HYGROMETERS = True
SHOW_ALARM = True SHOW_ALARM = True
SHOW_SMARTPLUGS = True SHOW_SMARTPLUGS = True
SHOW_LOCKS = True
CODE_DIGITS = 4 CODE_DIGITS = 4
# if wrong password was given don't try again # if wrong password was given don't try again
@ -63,11 +66,12 @@ def setup(hass, config):
from verisure import MyPages, LoginError, Error from verisure import MyPages, LoginError, Error
global SHOW_THERMOMETERS, SHOW_HYGROMETERS,\ 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_THERMOMETERS = int(config[DOMAIN].get('thermometers', '1'))
SHOW_HYGROMETERS = int(config[DOMAIN].get('hygrometers', '1')) SHOW_HYGROMETERS = int(config[DOMAIN].get('hygrometers', '1'))
SHOW_ALARM = int(config[DOMAIN].get('alarm', '1')) SHOW_ALARM = int(config[DOMAIN].get('alarm', '1'))
SHOW_SMARTPLUGS = int(config[DOMAIN].get('smartplugs', '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')) CODE_DIGITS = int(config[DOMAIN].get('code_digits', '4'))
global MY_PAGES global MY_PAGES
@ -87,11 +91,13 @@ def setup(hass, config):
update_alarm() update_alarm()
update_climate() update_climate()
update_smartplug() update_smartplug()
update_lock()
# Load components for the devices in the ISY controller that we support # Load components for the devices in the ISY controller that we support
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS), for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
('switch', DISCOVER_SWITCHES), ('switch', DISCOVER_SWITCHES),
('alarm_control_panel', DISCOVER_ALARMS))): ('alarm_control_panel', DISCOVER_ALARMS),
('lock', DISCOVER_LOCKS))):
component = get_component(comp_name) component = get_component(comp_name)
_LOGGER.info(config[DOMAIN]) _LOGGER.info(config[DOMAIN])
bootstrap.setup_component(hass, component.DOMAIN, config) bootstrap.setup_component(hass, component.DOMAIN, config)
@ -134,6 +140,11 @@ def update_smartplug():
update_component(MY_PAGES.smartplug.get, SMARTPLUG_STATUS) 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): def update_component(get_function, status):
""" Updates the status of verisure components. """ """ Updates the status of verisure components. """
if WRONG_PASSWORD_GIVEN: if WRONG_PASSWORD_GIVEN:

View File

@ -13,10 +13,10 @@ from homeassistant.helpers import validate_config
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import ( from homeassistant.const import (
EVENT_PLATFORM_DISCOVERED, CONF_ACCESS_TOKEN, EVENT_PLATFORM_DISCOVERED, CONF_ACCESS_TOKEN,
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME) ATTR_SERVICE, ATTR_DISCOVERED)
DOMAIN = "wink" DOMAIN = "wink"
REQUIREMENTS = ['python-wink==0.4.2'] REQUIREMENTS = ['python-wink==0.5.0']
DISCOVER_LIGHTS = "wink.lights" DISCOVER_LIGHTS = "wink.lights"
DISCOVER_SWITCHES = "wink.switches" DISCOVER_SWITCHES = "wink.switches"
@ -82,13 +82,6 @@ class WinkToggleDevice(ToggleEntity):
""" True if light is on. """ """ True if light is on. """
return self.wink.state() 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): def turn_on(self, **kwargs):
""" Turns the switch on. """ """ Turns the switch on. """
self.wink.set_state(True) self.wink.set_state(True)

View File

@ -200,3 +200,6 @@ HTTP_HEADER_EXPIRES = "Expires"
CONTENT_TYPE_JSON = "application/json" CONTENT_TYPE_JSON = "application/json"
CONTENT_TYPE_MULTIPART = 'multipart/x-mixed-replace; boundary={}' CONTENT_TYPE_MULTIPART = 'multipart/x-mixed-replace; boundary={}'
CONTENT_TYPE_TEXT_PLAIN = 'text/plain' 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 logging
import signal import signal
import threading import threading
from types import MappingProxyType
import enum import enum
import functools as ft import functools as ft
from collections import namedtuple
from homeassistant.const import ( from homeassistant.const import (
__version__, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, __version__, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
@ -20,7 +20,8 @@ from homeassistant.const import (
EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL, EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL,
EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED, 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 ( from homeassistant.exceptions import (
HomeAssistantError, InvalidEntityFormatError) HomeAssistantError, InvalidEntityFormatError)
import homeassistant.util as util import homeassistant.util as util
@ -45,12 +46,6 @@ MIN_WORKER_THREAD = 2
_LOGGER = logging.getLogger(__name__) _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): class HomeAssistant(object):
"""Root object of the Home Assistant home automation.""" """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?') 'Could not bind to SIGTERM. Are you running in a thread?')
while not request_shutdown.isSet(): while not request_shutdown.isSet():
try:
time.sleep(1) time.sleep(1)
except KeyboardInterrupt:
break
self.stop() self.stop()
return RESTART_EXIT_CODE if request_restart.isSet() else 0 return RESTART_EXIT_CODE if request_restart.isSet() else 0
@ -113,46 +111,6 @@ class HomeAssistant(object):
self.pool.stop() 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): class JobPriority(util.OrderedEnum):
"""Provides job priorities for event bus jobs.""" """Provides job priorities for event bus jobs."""
@ -352,7 +310,7 @@ class State(object):
self.entity_id = entity_id.lower() self.entity_id = entity_id.lower()
self.state = state self.state = state
self.attributes = attributes or {} self.attributes = MappingProxyType(attributes or {})
self.last_updated = dt_util.strip_microseconds( self.last_updated = dt_util.strip_microseconds(
last_updated or dt_util.utcnow()) last_updated or dt_util.utcnow())
@ -380,12 +338,6 @@ class State(object):
self.attributes.get(ATTR_FRIENDLY_NAME) or self.attributes.get(ATTR_FRIENDLY_NAME) or
self.object_id.replace('_', ' ')) 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): def as_dict(self):
"""Return a dict representation of the State. """Return a dict representation of the State.
@ -394,7 +346,7 @@ class State(object):
""" """
return {'entity_id': self.entity_id, return {'entity_id': self.entity_id,
'state': self.state, 'state': self.state,
'attributes': self.attributes, 'attributes': dict(self.attributes),
'last_changed': dt_util.datetime_to_str(self.last_changed), 'last_changed': dt_util.datetime_to_str(self.last_changed),
'last_updated': dt_util.datetime_to_str(self.last_updated)} 'last_updated': dt_util.datetime_to_str(self.last_updated)}
@ -458,14 +410,11 @@ class StateMachine(object):
def all(self): def all(self):
"""Create a list of all states.""" """Create a list of all states."""
with self._lock: with self._lock:
return [state.copy() for state in self._states.values()] return list(self._states.values())
def get(self, entity_id): def get(self, entity_id):
"""Retrieve state of entity_id or None if not found.""" """Retrieve state of entity_id or None if not found."""
state = self._states.get(entity_id.lower()) return self._states.get(entity_id.lower())
# Make a copy so people won't mutate the state
return state.copy() if state else None
def is_state(self, entity_id, state): def is_state(self, entity_id, state):
"""Test if entity exists and is specified state.""" """Test if entity exists and is specified state."""
@ -526,15 +475,6 @@ class StateMachine(object):
self._bus.fire(EVENT_STATE_CHANGED, event_data) 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 # pylint: disable=too-few-public-methods
class Service(object): class Service(object):

View File

@ -80,7 +80,20 @@ class Entity(object):
@property @property
def state_attributes(self): 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 return None
@property @property
@ -135,6 +148,11 @@ class Entity(object):
state = str(self.state) state = str(self.state)
attr = self.state_attributes or {} 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 \ if ATTR_UNIT_OF_MEASUREMENT not in attr and \
self.unit_of_measurement is not None: self.unit_of_measurement is not None:
attr[ATTR_UNIT_OF_MEASUREMENT] = str(self.unit_of_measurement) 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 # We group service calls for entities by service call
# json used to create a hashable version of dict with maybe lists in it # json used to create a hashable version of dict with maybe lists in it
key = (service_domain, service, 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) to_call[key].append(state.entity_id)
for (service_domain, service, service_data), entity_ids in to_call.items(): for (service_domain, service, service_data), entity_ids in to_call.items():

View File

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

View File

@ -48,7 +48,7 @@ def as_utc(dattim):
if dattim.tzinfo == UTC: if dattim.tzinfo == UTC:
return dattim return dattim
elif dattim.tzinfo is None: elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE) dattim = DEFAULT_TIME_ZONE.localize(dattim)
return dattim.astimezone(UTC) return dattim.astimezone(UTC)
@ -58,7 +58,7 @@ def as_local(dattim):
if dattim.tzinfo == DEFAULT_TIME_ZONE: if dattim.tzinfo == DEFAULT_TIME_ZONE:
return dattim return dattim
elif dattim.tzinfo is None: elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=UTC) dattim = UTC.localize(dattim)
return dattim.astimezone(DEFAULT_TIME_ZONE) 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 https://github.com/w1ll1am23/pygooglevoice-sms/archive/7c5ee9969b97a7992fc86a753fe9f20e3ffa3f7c.zip#pygooglevoice-sms==0.0.1
# homeassistant.components.influxdb # homeassistant.components.influxdb
influxdb==2.11.0 influxdb==2.12.0
# homeassistant.components.insteon_hub # homeassistant.components.insteon_hub
insteon_hub==0.4.5 insteon_hub==0.4.5
@ -105,6 +105,10 @@ liffylights==0.9.4
# homeassistant.components.light.limitlessled # homeassistant.components.light.limitlessled
limitlessled==1.0.0 limitlessled==1.0.0
# homeassistant.components.sensor.mfi
# homeassistant.components.switch.mfi
mficlient==0.2.2
# homeassistant.components.discovery # homeassistant.components.discovery
netdisco==0.5.2 netdisco==0.5.2
@ -153,6 +157,9 @@ pyicloud==0.7.2
# homeassistant.components.device_tracker.netgear # homeassistant.components.device_tracker.netgear
pynetgear==0.3.2 pynetgear==0.3.2
# homeassistant.components.alarm_control_panel.nx584
pynx584==0.1
# homeassistant.components.sensor.openweathermap # homeassistant.components.sensor.openweathermap
pyowm==2.3.0 pyowm==2.3.0
@ -183,13 +190,16 @@ python-telegram-bot==3.2.0
# homeassistant.components.sensor.twitch # homeassistant.components.sensor.twitch
python-twitch==1.2.0 python-twitch==1.2.0
# homeassistant.components.garage_door.wink
python-wink==0.4.2
# homeassistant.components.wink # homeassistant.components.wink
# homeassistant.components.garage_door.wink # homeassistant.components.garage_door.wink
# homeassistant.components.light.wink # homeassistant.components.light.wink
# homeassistant.components.lock.wink # homeassistant.components.lock.wink
# homeassistant.components.sensor.wink # homeassistant.components.sensor.wink
# homeassistant.components.switch.wink # homeassistant.components.switch.wink
python-wink==0.4.2 python-wink==0.5.0
# homeassistant.components.keyboard # homeassistant.components.keyboard
pyuserinput==0.1.9 pyuserinput==0.1.9
@ -197,10 +207,10 @@ pyuserinput==0.1.9
# homeassistant.components.light.vera # homeassistant.components.light.vera
# homeassistant.components.sensor.vera # homeassistant.components.sensor.vera
# homeassistant.components.switch.vera # homeassistant.components.switch.vera
pyvera==0.2.7 pyvera==0.2.8
# homeassistant.components.switch.wemo # homeassistant.components.switch.wemo
pywemo==0.3.8 pywemo==0.3.9
# homeassistant.components.thermostat.radiotherm # homeassistant.components.thermostat.radiotherm
radiotherm==1.2 radiotherm==1.2
@ -229,6 +239,9 @@ tellive-py==0.5.2
# homeassistant.components.switch.transmission # homeassistant.components.switch.transmission
transmissionrpc==0.11 transmissionrpc==0.11
# homeassistant.components.camera.uvc
uvcclient==0.5
# homeassistant.components.verisure # homeassistant.components.verisure
vsure==0.5.0 vsure==0.5.0

View File

@ -1,6 +1,6 @@
#!/bin/sh #!/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. # individual test file to run a single test.
cd "$(dirname "$0")/.." 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') grp2 = group.Group(self.hass, 'Je suis Charlie')
self.assertNotEqual(grp1.entity_id, grp2.entity_id) 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.assertEqual('test-payload',
self.calls[0][0].data['service_data'][mqtt.ATTR_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, { self.hass.bus.fire(EVENT_CALL_SERVICE, {
ATTR_DOMAIN: mqtt.DOMAIN, ATTR_DOMAIN: mqtt.DOMAIN,
ATTR_SERVICE: mqtt.SERVICE_PUBLISH ATTR_SERVICE: mqtt.SERVICE_PUBLISH
@ -76,6 +76,42 @@ class TestMQTT(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertTrue(not mqtt.MQTT_CLIENT.publish.called) 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): def test_subscribe_topic(self):
mqtt.subscribe(self.hass, 'test-topic', self.record_calls) 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.helpers.event import track_state_change
from homeassistant.const import ( from homeassistant.const import (
__version__, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, __version__, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
ATTR_FRIENDLY_NAME, TEMP_CELCIUS, EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, TEMP_CELCIUS,
TEMP_FAHRENHEIT) TEMP_FAHRENHEIT)
PST = pytz.timezone('America/Los_Angeles') PST = pytz.timezone('America/Los_Angeles')
@ -93,65 +93,6 @@ class TestHomeAssistant(unittest.TestCase):
self.assertEqual(1, len(calls)) 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): class TestEvent(unittest.TestCase):
""" Test Event class. """ """ Test Event class. """
@ -267,18 +208,6 @@ class TestState(unittest.TestCase):
{ATTR_FRIENDLY_NAME: name}) {ATTR_FRIENDLY_NAME: name})
self.assertEqual(name, state.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): def test_dict_conversion(self):
state = ha.State('domain.hello', 'world', {'some': 'attr'}) state = ha.State('domain.hello', 'world', {'some': 'attr'})
self.assertEqual(state, ha.State.from_dict(state.as_dict())) 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 # If it does not exist, we should get False
self.assertFalse(self.states.remove('light.Bowl')) 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): def test_case_insensitivty(self):
self.pool.add_worker() self.pool.add_worker()
runs = [] runs = []
track_state_change( self.bus.listen(EVENT_STATE_CHANGED, lambda event: runs.append(event))
ha._MockHA(self.bus), 'light.BoWl', lambda a, b, c: runs.append(1),
ha.MATCH_ALL, ha.MATCH_ALL)
self.states.set('light.BOWL', 'off') self.states.set('light.BOWL', 'off')
self.bus._pool.block_till_done() self.bus._pool.block_till_done()