Merge branch 'wink_garage_door_support' of https://github.com/xrolfex/home-assistant into wink_garage_door_support
commit
06cb97adee
homeassistant
components
alarm_control_panel
frontend
media_player
notify
thermostat
util
script
tests
|
@ -6,10 +6,14 @@ omit =
|
|||
|
||||
# omit pieces of code that rely on external devices being present
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
homeassistant/components/bloomsky.py
|
||||
homeassistant/components/*/bloomsky.py
|
||||
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
|
||||
|
@ -102,6 +106,7 @@ omit =
|
|||
homeassistant/components/notify/pushbullet.py
|
||||
homeassistant/components/notify/pushetta.py
|
||||
homeassistant/components/notify/pushover.py
|
||||
homeassistant/components/notify/rest.py
|
||||
homeassistant/components/notify/slack.py
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
|
|
|
@ -7,10 +7,12 @@ import sys
|
|||
import threading
|
||||
import os
|
||||
import argparse
|
||||
import time
|
||||
|
||||
from homeassistant import bootstrap
|
||||
import homeassistant.config as config_util
|
||||
from homeassistant.const import __version__, EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.const import (__version__, EVENT_HOMEASSISTANT_START,
|
||||
RESTART_EXIT_CODE)
|
||||
|
||||
|
||||
def validate_python():
|
||||
|
@ -76,6 +78,11 @@ def get_arguments():
|
|||
'--demo-mode',
|
||||
action='store_true',
|
||||
help='Start Home Assistant in demo mode')
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
help='Start Home Assistant in debug mode. Runs in single process to '
|
||||
'enable use of interactive debuggers.')
|
||||
parser.add_argument(
|
||||
'--open-ui',
|
||||
action='store_true',
|
||||
|
@ -207,8 +214,11 @@ def uninstall_osx():
|
|||
print("Home Assistant has been uninstalled.")
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir, args):
|
||||
""" Setup HASS and run. Block until stopped. """
|
||||
def setup_and_run_hass(config_dir, args, top_process=False):
|
||||
"""
|
||||
Setup HASS and run. Block until stopped. Will assume it is running in a
|
||||
subprocess unless top_process is set to true.
|
||||
"""
|
||||
if args.demo_mode:
|
||||
config = {
|
||||
'frontend': {},
|
||||
|
@ -235,7 +245,11 @@ def setup_and_run_hass(config_dir, args):
|
|||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
|
||||
|
||||
hass.start()
|
||||
sys.exit(int(hass.block_till_stopped()))
|
||||
exit_code = int(hass.block_till_stopped())
|
||||
|
||||
if not top_process:
|
||||
sys.exit(exit_code)
|
||||
return exit_code
|
||||
|
||||
|
||||
def run_hass_process(hass_proc):
|
||||
|
@ -243,8 +257,8 @@ def run_hass_process(hass_proc):
|
|||
requested_stop = threading.Event()
|
||||
hass_proc.daemon = True
|
||||
|
||||
def request_stop():
|
||||
""" request hass stop """
|
||||
def request_stop(*args):
|
||||
""" request hass stop, *args is for signal handler callback """
|
||||
requested_stop.set()
|
||||
hass_proc.terminate()
|
||||
|
||||
|
@ -262,7 +276,10 @@ def run_hass_process(hass_proc):
|
|||
hass_proc.join()
|
||||
except KeyboardInterrupt:
|
||||
return False
|
||||
return not requested_stop.isSet() and hass_proc.exitcode == 100
|
||||
|
||||
return (not requested_stop.isSet() and
|
||||
hass_proc.exitcode == RESTART_EXIT_CODE,
|
||||
hass_proc.exitcode)
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -277,14 +294,16 @@ def main():
|
|||
# os x launchd functions
|
||||
if args.install_osx:
|
||||
install_osx()
|
||||
return
|
||||
return 0
|
||||
if args.uninstall_osx:
|
||||
uninstall_osx()
|
||||
return
|
||||
return 0
|
||||
if args.restart_osx:
|
||||
uninstall_osx()
|
||||
# A small delay is needed on some systems to let the unload finish.
|
||||
time.sleep(0.5)
|
||||
install_osx()
|
||||
return
|
||||
return 0
|
||||
|
||||
# daemon functions
|
||||
if args.pid_file:
|
||||
|
@ -294,12 +313,23 @@ def main():
|
|||
if args.pid_file:
|
||||
write_pid(args.pid_file)
|
||||
|
||||
# Run hass in debug mode if requested
|
||||
if args.debug:
|
||||
sys.stderr.write('Running in debug mode. '
|
||||
'Home Assistant will not be able to restart.\n')
|
||||
exit_code = setup_and_run_hass(config_dir, args, top_process=True)
|
||||
if exit_code == RESTART_EXIT_CODE:
|
||||
sys.stderr.write('Home Assistant requested a '
|
||||
'restart in debug mode.\n')
|
||||
return exit_code
|
||||
|
||||
# Run hass as child process. Restart if necessary.
|
||||
keep_running = True
|
||||
while keep_running:
|
||||
hass_proc = Process(target=setup_and_run_hass, args=(config_dir, args))
|
||||
keep_running = run_hass_process(hass_proc)
|
||||
keep_running, exit_code = run_hass_process(hass_proc)
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(main())
|
||||
|
|
|
@ -61,6 +61,8 @@ def setup(hass, config):
|
|||
|
||||
for alarm in target_alarms:
|
||||
getattr(alarm, method)(code)
|
||||
if alarm.should_poll:
|
||||
alarm.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
|
|
@ -90,7 +90,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.disarm()
|
||||
self.update_ha_state()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
|
@ -100,7 +99,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.arm_stay()
|
||||
self.update_ha_state()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
|
@ -110,7 +108,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.arm_away()
|
||||
self.update_ha_state()
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
""" Validate given code. """
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
||||
})
|
|
@ -33,8 +33,6 @@ SWITCH_ACTION_SNAPSHOT = 'snapshot'
|
|||
|
||||
SERVICE_CAMERA = 'camera_service'
|
||||
|
||||
STATE_RECORDING = 'recording'
|
||||
|
||||
DEFAULT_RECORDING_SECONDS = 30
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
|
@ -46,6 +44,7 @@ DIR_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S'
|
|||
REC_DIR_PREFIX = 'recording-'
|
||||
REC_IMG_PREFIX = 'recording_image-'
|
||||
|
||||
STATE_RECORDING = 'recording'
|
||||
STATE_STREAMING = 'streaming'
|
||||
STATE_IDLE = 'idle'
|
||||
|
||||
|
@ -121,33 +120,7 @@ def setup(hass, config):
|
|||
try:
|
||||
camera.is_streaming = True
|
||||
camera.update_ha_state()
|
||||
|
||||
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
|
||||
handler.request.sendall(bytes(
|
||||
'Content-type: multipart/x-mixed-replace; \
|
||||
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
|
||||
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
|
||||
# MJPEG_START_HEADER.format()
|
||||
|
||||
while True:
|
||||
img_bytes = camera.camera_image()
|
||||
if img_bytes is None:
|
||||
continue
|
||||
headers_str = '\r\n'.join((
|
||||
'Content-length: {}'.format(len(img_bytes)),
|
||||
'Content-type: image/jpeg',
|
||||
)) + '\r\n\r\n'
|
||||
|
||||
handler.request.sendall(
|
||||
bytes(headers_str, 'utf-8') +
|
||||
img_bytes +
|
||||
bytes('\r\n', 'utf-8'))
|
||||
|
||||
handler.request.sendall(
|
||||
bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
|
||||
time.sleep(0.5)
|
||||
camera.mjpeg_stream(handler)
|
||||
|
||||
except (requests.RequestException, IOError):
|
||||
camera.is_streaming = False
|
||||
|
@ -190,6 +163,34 @@ class Camera(Entity):
|
|||
""" Return bytes of camera image. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def mjpeg_stream(self, handler):
|
||||
""" Generate an HTTP MJPEG stream from camera images. """
|
||||
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
|
||||
handler.request.sendall(bytes(
|
||||
'Content-type: multipart/x-mixed-replace; \
|
||||
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
|
||||
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
|
||||
# MJPEG_START_HEADER.format()
|
||||
while True:
|
||||
img_bytes = self.camera_image()
|
||||
if img_bytes is None:
|
||||
continue
|
||||
headers_str = '\r\n'.join((
|
||||
'Content-length: {}'.format(len(img_bytes)),
|
||||
'Content-type: image/jpeg',
|
||||
)) + '\r\n\r\n'
|
||||
|
||||
handler.request.sendall(
|
||||
bytes(headers_str, 'utf-8') +
|
||||
img_bytes +
|
||||
bytes('\r\n', 'utf-8'))
|
||||
|
||||
handler.request.sendall(
|
||||
bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the entity. """
|
||||
|
|
|
@ -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
|
|
@ -14,6 +14,9 @@ from requests.auth import HTTPBasicAuth
|
|||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
from homeassistant.const import HTTP_OK
|
||||
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -41,6 +44,17 @@ class MjpegCamera(Camera):
|
|||
self._password = device_info.get('password')
|
||||
self._mjpeg_url = device_info['mjpeg_url']
|
||||
|
||||
def camera_stream(self):
|
||||
""" Return a mjpeg stream image response directly from the camera. """
|
||||
if self._username and self._password:
|
||||
return requests.get(self._mjpeg_url,
|
||||
auth=HTTPBasicAuth(self._username,
|
||||
self._password),
|
||||
stream=True)
|
||||
else:
|
||||
return requests.get(self._mjpeg_url,
|
||||
stream=True)
|
||||
|
||||
def camera_image(self):
|
||||
""" Return a still image response from the camera. """
|
||||
|
||||
|
@ -55,16 +69,22 @@ class MjpegCamera(Camera):
|
|||
jpg = data[jpg_start:jpg_end + 2]
|
||||
return jpg
|
||||
|
||||
if self._username and self._password:
|
||||
with closing(requests.get(self._mjpeg_url,
|
||||
auth=HTTPBasicAuth(self._username,
|
||||
self._password),
|
||||
stream=True)) as response:
|
||||
return process_response(response)
|
||||
else:
|
||||
with closing(requests.get(self._mjpeg_url,
|
||||
stream=True)) as response:
|
||||
return process_response(response)
|
||||
with closing(self.camera_stream()) as response:
|
||||
return process_response(response)
|
||||
|
||||
def mjpeg_stream(self, handler):
|
||||
""" Generate an HTTP MJPEG stream from the camera. """
|
||||
response = self.camera_stream()
|
||||
content_type = response.headers[CONTENT_TYPE_HEADER]
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.send_header(CONTENT_TYPE_HEADER, content_type)
|
||||
handler.end_headers()
|
||||
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
if not chunk:
|
||||
break
|
||||
handler.wfile.write(chunk)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
|
@ -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()
|
|
@ -141,7 +141,7 @@ class Configurator(object):
|
|||
|
||||
state = self.hass.states.get(entity_id)
|
||||
|
||||
new_data = state.attributes
|
||||
new_data = dict(state.attributes)
|
||||
new_data[ATTR_ERRORS] = error
|
||||
|
||||
self.hass.states.set(entity_id, STATE_CONFIGURE, new_data)
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "b5daaa4815050f90f6c996a429bfeae1"
|
||||
VERSION = "e310ed31e0c6d96def74b44c90ff5878"
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
Subproject commit 8050e0861586a65317de27dbfd0ff50ffe209731
|
||||
Subproject commit 31fb734c5b080797e81b9143e2db530c70390f5a
|
|
@ -1,5 +1,5 @@
|
|||
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={exports:{},id:r,loaded:!1};return e[r].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([/*!*************************************!*\
|
||||
!*** ./src/service-worker/index.js ***!
|
||||
\*************************************/
|
||||
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){var r=fetch(e.request).then(function(e){return t.put(s,e.clone()),e});return n||r})}))})}]);
|
||||
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}]);
|
||||
//# sourceMappingURL=service_worker.js.map
|
|
@ -71,7 +71,7 @@ def expand_entity_ids(hass, entity_ids):
|
|||
if domain == DOMAIN:
|
||||
found_ids.extend(
|
||||
ent_id for ent_id
|
||||
in get_entity_ids(hass, entity_id)
|
||||
in expand_entity_ids(hass, get_entity_ids(hass, entity_id))
|
||||
if ent_id not in found_ids)
|
||||
|
||||
else:
|
||||
|
|
|
@ -25,7 +25,7 @@ DEFAULT_DATABASE = 'home_assistant'
|
|||
DEFAULT_SSL = False
|
||||
DEFAULT_VERIFY_SSL = False
|
||||
|
||||
REQUIREMENTS = ['influxdb==2.11.0']
|
||||
REQUIREMENTS = ['influxdb==2.12.0']
|
||||
|
||||
CONF_HOST = 'host'
|
||||
CONF_PORT = 'port'
|
||||
|
@ -70,25 +70,22 @@ def setup(hass, config):
|
|||
""" Listen for new messages on the bus and sends them to Influx. """
|
||||
|
||||
state = event.data.get('new_state')
|
||||
|
||||
if state is None:
|
||||
if state is None or state.state in (STATE_UNKNOWN, ''):
|
||||
return
|
||||
|
||||
if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON):
|
||||
_state = 1
|
||||
elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_UNKNOWN,
|
||||
STATE_BELOW_HORIZON):
|
||||
elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_BELOW_HORIZON):
|
||||
_state = 0
|
||||
else:
|
||||
_state = state.state
|
||||
if _state == '':
|
||||
return
|
||||
try:
|
||||
_state = float(_state)
|
||||
_state = float(state.state)
|
||||
except ValueError:
|
||||
pass
|
||||
_state = state.state
|
||||
|
||||
measurement = state.attributes.get('unit_of_measurement', state.domain)
|
||||
measurement = state.attributes.get('unit_of_measurement')
|
||||
if measurement in (None, ''):
|
||||
measurement = state.entity_id
|
||||
|
||||
json_body = [
|
||||
{
|
||||
|
|
|
@ -300,11 +300,6 @@ class Light(ToggleEntity):
|
|||
""" CT color value in mirads. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Returns device specific state attributes. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
|
@ -322,9 +317,4 @@ class Light(ToggleEntity):
|
|||
data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1],
|
||||
data[ATTR_BRIGHTNESS])
|
||||
|
||||
device_attr = self.device_state_attributes
|
||||
|
||||
if device_attr is not None:
|
||||
data.update(device_attr)
|
||||
|
||||
return data
|
||||
|
|
|
@ -9,13 +9,20 @@ https://home-assistant.io/components/light.vera/
|
|||
import logging
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
from homeassistant.components.switch.vera import VeraSwitch
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_ON
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL,
|
||||
ATTR_TRIPPED,
|
||||
ATTR_ARMED,
|
||||
ATTR_LAST_TRIP_TIME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_ON,
|
||||
STATE_OFF)
|
||||
|
||||
REQUIREMENTS = ['pyvera==0.2.7']
|
||||
REQUIREMENTS = ['pyvera==0.2.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -67,17 +74,35 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
add_devices_callback(lights)
|
||||
|
||||
|
||||
class VeraLight(VeraSwitch):
|
||||
class VeraLight(Light):
|
||||
""" Represents a Vera Light, including dimmable. """
|
||||
|
||||
def __init__(self, vera_device, controller, extra_data=None):
|
||||
self.vera_device = vera_device
|
||||
self.extra_data = extra_data
|
||||
self.controller = controller
|
||||
if self.extra_data and self.extra_data.get('name'):
|
||||
self._name = self.extra_data.get('name')
|
||||
else:
|
||||
self._name = self.vera_device.name
|
||||
self._state = STATE_OFF
|
||||
|
||||
self.controller.register(vera_device, self._update_callback)
|
||||
self.update()
|
||||
|
||||
def _update_callback(self, _device):
|
||||
self.update_ha_state(True)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
attr = super().state_attributes or {}
|
||||
def name(self):
|
||||
""" Get the mame of the switch. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Brightness of the light."""
|
||||
if self.vera_device.is_dimmable:
|
||||
attr[ATTR_BRIGHTNESS] = self.vera_device.get_brightness()
|
||||
|
||||
return attr
|
||||
return self.vera_device.get_brightness()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
if ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable:
|
||||
|
@ -87,3 +112,49 @@ class VeraLight(VeraSwitch):
|
|||
|
||||
self._state = STATE_ON
|
||||
self.update_ha_state(True)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
self.vera_device.switch_off()
|
||||
self._state = STATE_OFF
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
attr = {}
|
||||
|
||||
if self.vera_device.has_battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
||||
|
||||
if self.vera_device.is_armable:
|
||||
armed = self.vera_device.is_armed
|
||||
attr[ATTR_ARMED] = 'True' if armed else 'False'
|
||||
|
||||
if self.vera_device.is_trippable:
|
||||
last_tripped = self.vera_device.last_trip
|
||||
if last_tripped is not None:
|
||||
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
|
||||
attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str(
|
||||
utc_time)
|
||||
else:
|
||||
attr[ATTR_LAST_TRIP_TIME] = None
|
||||
tripped = self.vera_device.is_tripped
|
||||
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
|
||||
|
||||
attr['Vera Device Id'] = self.vera_device.vera_device_id
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Tells Home Assistant not to poll this entity. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state == STATE_ON
|
||||
|
||||
def update(self):
|
||||
""" Called by the vera device callback to update state. """
|
||||
if self.vera_device.is_switched_on():
|
||||
self._state = STATE_ON
|
||||
else:
|
||||
self._state = STATE_OFF
|
||||
|
|
|
@ -8,11 +8,10 @@ https://home-assistant.io/components/light.wink/
|
|||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
from homeassistant.components.wink import WinkToggleDevice
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.4.2']
|
||||
REQUIREMENTS = ['python-wink==0.5.0']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
@ -34,9 +33,32 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
WinkLight(light) for light in pywink.get_bulbs())
|
||||
|
||||
|
||||
class WinkLight(WinkToggleDevice):
|
||||
class WinkLight(Light):
|
||||
""" Represents a Wink light. """
|
||||
|
||||
def __init__(self, wink):
|
||||
self.wink = wink
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
""" Returns the id of this Wink switch. """
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the light if any. """
|
||||
return self.wink.name()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if light is on. """
|
||||
return self.wink.state()
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Brightness of the light."""
|
||||
return int(self.wink.brightness() * 255)
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
|
@ -48,14 +70,10 @@ class WinkLight(WinkToggleDevice):
|
|||
else:
|
||||
self.wink.set_state(True)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
attr = super().state_attributes
|
||||
def turn_off(self):
|
||||
""" Turns the switch off. """
|
||||
self.wink.set_state(False)
|
||||
|
||||
if self.is_on:
|
||||
brightness = self.wink.brightness()
|
||||
|
||||
if brightness is not None:
|
||||
attr[ATTR_BRIGHTNESS] = int(brightness * 255)
|
||||
|
||||
return attr
|
||||
def update(self):
|
||||
""" Update state of the light. """
|
||||
self.wink.update_state()
|
||||
|
|
|
@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity
|
|||
from homeassistant.const import (
|
||||
STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK,
|
||||
ATTR_ENTITY_ID)
|
||||
from homeassistant.components import (group, wink)
|
||||
from homeassistant.components import (group, verisure, wink)
|
||||
|
||||
DOMAIN = 'lock'
|
||||
SCAN_INTERVAL = 30
|
||||
|
@ -28,12 +28,15 @@ ENTITY_ID_ALL_LOCKS = group.ENTITY_ID_FORMAT.format('all_locks')
|
|||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
ATTR_LOCKED = "locked"
|
||||
ATTR_CODE = 'code'
|
||||
ATTR_CODE_FORMAT = 'code_format'
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
wink.DISCOVER_LOCKS: 'wink'
|
||||
wink.DISCOVER_LOCKS: 'wink',
|
||||
verisure.DISCOVER_LOCKS: 'verisure'
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -45,15 +48,25 @@ def is_locked(hass, entity_id=None):
|
|||
return hass.states.is_state(entity_id, STATE_LOCKED)
|
||||
|
||||
|
||||
def lock(hass, entity_id=None):
|
||||
def lock(hass, entity_id=None, code=None):
|
||||
""" Locks all or specified locks. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_LOCK, data)
|
||||
|
||||
|
||||
def unlock(hass, entity_id=None):
|
||||
def unlock(hass, entity_id=None, code=None):
|
||||
""" Unlocks all or specified locks. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_UNLOCK, data)
|
||||
|
||||
|
||||
|
@ -68,11 +81,16 @@ def setup(hass, config):
|
|||
""" Handles calls to the lock services. """
|
||||
target_locks = component.extract_from_service(service)
|
||||
|
||||
if ATTR_CODE not in service.data:
|
||||
code = None
|
||||
else:
|
||||
code = service.data[ATTR_CODE]
|
||||
|
||||
for item in target_locks:
|
||||
if service.service == SERVICE_LOCK:
|
||||
item.lock()
|
||||
item.lock(code=code)
|
||||
else:
|
||||
item.unlock()
|
||||
item.unlock(code=code)
|
||||
|
||||
if item.should_poll:
|
||||
item.update_ha_state(True)
|
||||
|
@ -91,19 +109,34 @@ class LockDevice(Entity):
|
|||
""" Represents a lock within Home Assistant. """
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" regex for code format or None if no code is required. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
""" Is the lock locked or unlocked. """
|
||||
return None
|
||||
|
||||
def lock(self):
|
||||
def lock(self, **kwargs):
|
||||
""" Locks the lock. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def unlock(self):
|
||||
def unlock(self, **kwargs):
|
||||
""" Unlocks the lock. """
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Return the state attributes. """
|
||||
if self.code_format is None:
|
||||
return None
|
||||
state_attr = {
|
||||
ATTR_CODE_FORMAT: self.code_format,
|
||||
}
|
||||
return state_attr
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
locked = self.is_locked
|
||||
|
|
|
@ -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()
|
|
@ -11,7 +11,7 @@ import logging
|
|||
from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.4.2']
|
||||
REQUIREMENTS = ['python-wink==0.5.0']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
|
|
@ -425,11 +425,6 @@ class MediaPlayerDevice(Entity):
|
|||
""" Flags of media commands that are supported. """
|
||||
return 0
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Extra attributes a device wants to expose. """
|
||||
return None
|
||||
|
||||
def turn_on(self):
|
||||
""" turn the media player on. """
|
||||
raise NotImplementedError()
|
||||
|
@ -546,9 +541,4 @@ class MediaPlayerDevice(Entity):
|
|||
if self.media_image_url:
|
||||
state_attr[ATTR_ENTITY_PICTURE] = self.media_image_url
|
||||
|
||||
device_attr = self.device_state_attributes
|
||||
|
||||
if device_attr:
|
||||
state_attr.update(device_attr)
|
||||
|
||||
return state_attr
|
||||
|
|
|
@ -56,8 +56,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
hosts = [(config[CONF_HOST], DEFAULT_PORT)]
|
||||
|
||||
else:
|
||||
hosts = [host for host in pychromecast.discover_chromecasts()
|
||||
if host not in KNOWN_HOSTS]
|
||||
hosts = [tuple(dev[:2]) for dev in pychromecast.discover_chromecasts()
|
||||
if tuple(dev[:2]) not in KNOWN_HOSTS]
|
||||
|
||||
casts = []
|
||||
|
||||
|
|
|
@ -12,8 +12,10 @@ import socket
|
|||
import time
|
||||
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.util as util
|
||||
from homeassistant.util import template
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
|
@ -49,24 +51,34 @@ DEFAULT_PROTOCOL = PROTOCOL_311
|
|||
|
||||
ATTR_TOPIC = 'topic'
|
||||
ATTR_PAYLOAD = 'payload'
|
||||
ATTR_PAYLOAD_TEMPLATE = 'payload_template'
|
||||
ATTR_QOS = 'qos'
|
||||
ATTR_RETAIN = 'retain'
|
||||
|
||||
MAX_RECONNECT_WAIT = 300 # seconds
|
||||
|
||||
|
||||
def publish(hass, topic, payload, qos=None, retain=None):
|
||||
"""Publish message to an MQTT topic."""
|
||||
data = {
|
||||
ATTR_TOPIC: topic,
|
||||
ATTR_PAYLOAD: payload,
|
||||
}
|
||||
def _build_publish_data(topic, qos, retain):
|
||||
"""Build the arguments for the publish service without the payload."""
|
||||
data = {ATTR_TOPIC: topic}
|
||||
if qos is not None:
|
||||
data[ATTR_QOS] = qos
|
||||
|
||||
if retain is not None:
|
||||
data[ATTR_RETAIN] = retain
|
||||
return data
|
||||
|
||||
|
||||
def publish(hass, topic, payload, qos=None, retain=None):
|
||||
"""Publish message to an MQTT topic."""
|
||||
data = _build_publish_data(topic, qos, retain)
|
||||
data[ATTR_PAYLOAD] = payload
|
||||
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
|
||||
|
||||
|
||||
def publish_template(hass, topic, payload_template, qos=None, retain=None):
|
||||
"""Publish message to an MQTT topic using a template payload."""
|
||||
data = _build_publish_data(topic, qos, retain)
|
||||
data[ATTR_PAYLOAD_TEMPLATE] = payload_template
|
||||
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
|
||||
|
||||
|
||||
|
@ -132,15 +144,34 @@ def setup(hass, config):
|
|||
"""Handle MQTT publish service calls."""
|
||||
msg_topic = call.data.get(ATTR_TOPIC)
|
||||
payload = call.data.get(ATTR_PAYLOAD)
|
||||
payload_template = call.data.get(ATTR_PAYLOAD_TEMPLATE)
|
||||
qos = call.data.get(ATTR_QOS, DEFAULT_QOS)
|
||||
retain = call.data.get(ATTR_RETAIN, DEFAULT_RETAIN)
|
||||
if payload is None:
|
||||
if payload_template is None:
|
||||
_LOGGER.error(
|
||||
"You must set either '%s' or '%s' to use this service",
|
||||
ATTR_PAYLOAD, ATTR_PAYLOAD_TEMPLATE)
|
||||
return
|
||||
try:
|
||||
payload = template.render(hass, payload_template)
|
||||
except template.jinja2.TemplateError as exc:
|
||||
_LOGGER.error(
|
||||
"Unable to publish to '%s': rendering payload template of "
|
||||
"'%s' failed because %s.",
|
||||
msg_topic, payload_template, exc)
|
||||
return
|
||||
if msg_topic is None or payload is None:
|
||||
return
|
||||
MQTT_CLIENT.publish(msg_topic, payload, qos, retain)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_mqtt)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_PUBLISH, publish_service)
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_PUBLISH, publish_service,
|
||||
descriptions.get(SERVICE_PUBLISH))
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
@ -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
|
|
@ -24,7 +24,9 @@ CONF_DEBUG = 'debug'
|
|||
CONF_PERSISTENCE = 'persistence'
|
||||
CONF_PERSISTENCE_FILE = 'persistence_file'
|
||||
CONF_VERSION = 'version'
|
||||
CONF_BAUD_RATE = 'baud_rate'
|
||||
DEFAULT_VERSION = '1.4'
|
||||
DEFAULT_BAUD_RATE = 115200
|
||||
|
||||
DOMAIN = 'mysensors'
|
||||
DEPENDENCIES = []
|
||||
|
@ -60,12 +62,13 @@ def setup(hass, config):
|
|||
version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION))
|
||||
is_metric = (hass.config.temperature_unit == TEMP_CELCIUS)
|
||||
|
||||
def setup_gateway(port, persistence, persistence_file, version):
|
||||
def setup_gateway(port, persistence, persistence_file, version, baud_rate):
|
||||
"""Return gateway after setup of the gateway."""
|
||||
gateway = mysensors.SerialGateway(port, event_callback=None,
|
||||
persistence=persistence,
|
||||
persistence_file=persistence_file,
|
||||
protocol_version=version)
|
||||
protocol_version=version,
|
||||
baud=baud_rate)
|
||||
gateway.metric = is_metric
|
||||
gateway.debug = config[DOMAIN].get(CONF_DEBUG, False)
|
||||
gateway = GatewayWrapper(gateway, version)
|
||||
|
@ -98,8 +101,9 @@ def setup(hass, config):
|
|||
persistence_file = gway.get(
|
||||
CONF_PERSISTENCE_FILE,
|
||||
hass.config.path('mysensors{}.pickle'.format(index + 1)))
|
||||
baud_rate = gway.get(CONF_BAUD_RATE, DEFAULT_BAUD_RATE)
|
||||
GATEWAYS[port] = setup_gateway(
|
||||
port, persistence, persistence_file, version)
|
||||
port, persistence, persistence_file, version, baud_rate)
|
||||
|
||||
for (component, discovery_service) in DISCOVERY_COMPONENTS:
|
||||
# Ensure component is loaded
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -232,7 +232,7 @@ class Recorder(threading.Thread):
|
|||
else:
|
||||
state_domain = state.domain
|
||||
state_state = state.state
|
||||
state_attr = json.dumps(state.attributes)
|
||||
state_attr = json.dumps(dict(state.attributes))
|
||||
last_changed = state.last_changed
|
||||
last_updated = state.last_updated
|
||||
|
||||
|
@ -285,7 +285,8 @@ class Recorder(threading.Thread):
|
|||
else:
|
||||
return cur.fetchall()
|
||||
|
||||
except sqlite3.IntegrityError:
|
||||
except (sqlite3.IntegrityError, sqlite3.OperationalError,
|
||||
sqlite3.ProgrammingError):
|
||||
_LOGGER.exception(
|
||||
"Error querying the database using: %s", sql_query)
|
||||
return []
|
||||
|
@ -329,7 +330,7 @@ class Recorder(threading.Thread):
|
|||
migration_id = 0
|
||||
|
||||
if migration_id < 1:
|
||||
self.query("""
|
||||
cur.execute("""
|
||||
CREATE TABLE recorder_runs (
|
||||
run_id integer primary key,
|
||||
start integer,
|
||||
|
@ -338,7 +339,7 @@ class Recorder(threading.Thread):
|
|||
created integer)
|
||||
""")
|
||||
|
||||
self.query("""
|
||||
cur.execute("""
|
||||
CREATE TABLE events (
|
||||
event_id integer primary key,
|
||||
event_type text,
|
||||
|
@ -346,10 +347,10 @@ class Recorder(threading.Thread):
|
|||
origin text,
|
||||
created integer)
|
||||
""")
|
||||
self.query(
|
||||
cur.execute(
|
||||
'CREATE INDEX events__event_type ON events(event_type)')
|
||||
|
||||
self.query("""
|
||||
cur.execute("""
|
||||
CREATE TABLE states (
|
||||
state_id integer primary key,
|
||||
entity_id text,
|
||||
|
@ -359,51 +360,51 @@ class Recorder(threading.Thread):
|
|||
last_updated integer,
|
||||
created integer)
|
||||
""")
|
||||
self.query('CREATE INDEX states__entity_id ON states(entity_id)')
|
||||
cur.execute('CREATE INDEX states__entity_id ON states(entity_id)')
|
||||
|
||||
save_migration(1)
|
||||
|
||||
if migration_id < 2:
|
||||
self.query("""
|
||||
cur.execute("""
|
||||
ALTER TABLE events
|
||||
ADD COLUMN time_fired integer
|
||||
""")
|
||||
|
||||
self.query('UPDATE events SET time_fired=created')
|
||||
cur.execute('UPDATE events SET time_fired=created')
|
||||
|
||||
save_migration(2)
|
||||
|
||||
if migration_id < 3:
|
||||
utc_offset = self.utc_offset
|
||||
|
||||
self.query("""
|
||||
cur.execute("""
|
||||
ALTER TABLE recorder_runs
|
||||
ADD COLUMN utc_offset integer
|
||||
""")
|
||||
|
||||
self.query("""
|
||||
cur.execute("""
|
||||
ALTER TABLE events
|
||||
ADD COLUMN utc_offset integer
|
||||
""")
|
||||
|
||||
self.query("""
|
||||
cur.execute("""
|
||||
ALTER TABLE states
|
||||
ADD COLUMN utc_offset integer
|
||||
""")
|
||||
|
||||
self.query("UPDATE recorder_runs SET utc_offset=?", [utc_offset])
|
||||
self.query("UPDATE events SET utc_offset=?", [utc_offset])
|
||||
self.query("UPDATE states SET utc_offset=?", [utc_offset])
|
||||
cur.execute("UPDATE recorder_runs SET utc_offset=?", [utc_offset])
|
||||
cur.execute("UPDATE events SET utc_offset=?", [utc_offset])
|
||||
cur.execute("UPDATE states SET utc_offset=?", [utc_offset])
|
||||
|
||||
save_migration(3)
|
||||
|
||||
if migration_id < 4:
|
||||
# We had a bug where we did not save utc offset for recorder runs
|
||||
self.query(
|
||||
cur.execute(
|
||||
"""UPDATE recorder_runs SET utc_offset=?
|
||||
WHERE utc_offset IS NULL""", [self.utc_offset])
|
||||
|
||||
self.query("""
|
||||
cur.execute("""
|
||||
ALTER TABLE states
|
||||
ADD COLUMN event_id integer
|
||||
""")
|
||||
|
@ -412,25 +413,33 @@ class Recorder(threading.Thread):
|
|||
|
||||
if migration_id < 5:
|
||||
# Add domain so that thermostat graphs look right
|
||||
self.query("""
|
||||
ALTER TABLE states
|
||||
ADD COLUMN domain text
|
||||
""")
|
||||
try:
|
||||
cur.execute("""
|
||||
ALTER TABLE states
|
||||
ADD COLUMN domain text
|
||||
""")
|
||||
except sqlite3.OperationalError:
|
||||
# We had a bug in this migration for a while on dev
|
||||
# Without this, dev-users will have to throw away their db
|
||||
pass
|
||||
|
||||
# TravisCI has Python compiled against an old version of SQLite3
|
||||
# which misses the instr method.
|
||||
self.conn.create_function(
|
||||
"instr", 2,
|
||||
lambda string, substring: string.find(substring) + 1)
|
||||
|
||||
# populate domain with defaults
|
||||
rows = self.query("select distinct entity_id from states")
|
||||
for row in rows:
|
||||
entity_id = row[0]
|
||||
domain = entity_id.split(".")[0]
|
||||
self.query(
|
||||
"UPDATE states set domain=? where entity_id=?",
|
||||
domain, entity_id)
|
||||
cur.execute("""
|
||||
UPDATE states
|
||||
set domain=substr(entity_id, 0, instr(entity_id, '.'))
|
||||
""")
|
||||
|
||||
# add indexes we are going to use a lot on selects
|
||||
self.query("""
|
||||
cur.execute("""
|
||||
CREATE INDEX states__state_changes ON
|
||||
states (last_changed, last_updated, entity_id)""")
|
||||
self.query("""
|
||||
cur.execute("""
|
||||
CREATE INDEX states__significant_changes ON
|
||||
states (domain, last_updated, entity_id)""")
|
||||
save_migration(5)
|
||||
|
|
|
@ -39,6 +39,7 @@ OPTION_TYPES = {
|
|||
'miners_revenue_btc': ['Miners revenue', 'BTC'],
|
||||
'market_price_usd': ['Market price', 'USD']
|
||||
}
|
||||
ICON = 'mdi:currency-btc'
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
||||
|
@ -108,6 +109,11 @@ class BitcoinSensor(Entity):
|
|||
def unit_of_measurement(self):
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
""" Icon to use in the frontend, if any. """
|
||||
return ICON
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def update(self):
|
||||
""" Gets the latest data and updates the states. """
|
||||
|
|
|
@ -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
|
|
@ -54,7 +54,7 @@ class CpuSpeedSensor(Entity):
|
|||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def device_state_attributes(self):
|
||||
""" Returns the state attributes. """
|
||||
if self.info is not None:
|
||||
return {
|
||||
|
|
|
@ -46,7 +46,7 @@ class DemoSensor(Entity):
|
|||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def device_state_attributes(self):
|
||||
""" Returns the state attributes. """
|
||||
if self._battery:
|
||||
return {
|
||||
|
|
|
@ -23,6 +23,7 @@ SENSOR_TYPES = {
|
|||
'temperature': ['Temperature', None],
|
||||
'humidity': ['Humidity', '%']
|
||||
}
|
||||
DEFAULT_NAME = "DHT Sensor"
|
||||
# Return cached results if last scan was less then this time ago
|
||||
# DHT11 is able to deliver data once per second, DHT22 once every two
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||
|
@ -53,12 +54,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
data = DHTClient(Adafruit_DHT, sensor, pin)
|
||||
dev = []
|
||||
name = config.get('name', DEFAULT_NAME)
|
||||
|
||||
try:
|
||||
for variable in config['monitored_conditions']:
|
||||
if variable not in SENSOR_TYPES:
|
||||
_LOGGER.error('Sensor type: "%s" does not exist', variable)
|
||||
else:
|
||||
dev.append(DHTSensor(data, variable, unit))
|
||||
dev.append(DHTSensor(data, variable, unit, name))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
@ -69,8 +72,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class DHTSensor(Entity):
|
||||
""" Implements an DHT sensor. """
|
||||
|
||||
def __init__(self, dht_client, sensor_type, temp_unit):
|
||||
self.client_name = 'DHT sensor'
|
||||
def __init__(self, dht_client, sensor_type, temp_unit, name):
|
||||
self.client_name = name
|
||||
self._name = SENSOR_TYPES[sensor_type][0]
|
||||
self.dht_client = dht_client
|
||||
self.temp_unit = temp_unit
|
||||
|
|
|
@ -63,6 +63,11 @@ class EcobeeSensor(Entity):
|
|||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Unique id of this sensor."""
|
||||
return "sensor_ecobee_{}_{}".format(self.type, self.index)
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Unit of measurement this sensor expresses itself in. """
|
||||
|
|
|
@ -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()
|
|
@ -7,7 +7,7 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/sensor.mqtt/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE, STATE_UNKNOWN
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import template
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
|
@ -42,7 +42,7 @@ class MqttSensor(Entity):
|
|||
""" Represents a sensor that can be updated using MQTT. """
|
||||
def __init__(self, hass, name, state_topic, qos, unit_of_measurement,
|
||||
value_template):
|
||||
self._state = "-"
|
||||
self._state = STATE_UNKNOWN
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
|
|
|
@ -151,41 +151,39 @@ class MySensorsSensor(Entity):
|
|||
self.gateway.const.SetReq.V_VOLTAGE: 'V',
|
||||
self.gateway.const.SetReq.V_CURRENT: 'A',
|
||||
}
|
||||
unit_map_v15 = {
|
||||
self.gateway.const.SetReq.V_PERCENTAGE: '%',
|
||||
}
|
||||
if float(self.gateway.version) >= 1.5:
|
||||
if self.gateway.const.SetReq.V_UNIT_PREFIX in self._values:
|
||||
return self._values[
|
||||
self.gateway.const.SetReq.V_UNIT_PREFIX]
|
||||
unit_map.update(unit_map_v15)
|
||||
unit_map.update({self.gateway.const.SetReq.V_PERCENTAGE: '%'})
|
||||
return unit_map.get(self.value_type)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
device_attr = {}
|
||||
for value_type, value in self._values.items():
|
||||
if value_type != self.value_type:
|
||||
device_attr[self.gateway.const.SetReq(value_type).name] = value
|
||||
return device_attr
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
data = {
|
||||
attr = {
|
||||
mysensors.ATTR_PORT: self.gateway.port,
|
||||
mysensors.ATTR_NODE_ID: self.node_id,
|
||||
mysensors.ATTR_CHILD_ID: self.child_id,
|
||||
ATTR_BATTERY_LEVEL: self.battery_level,
|
||||
}
|
||||
|
||||
device_attr = self.device_state_attributes
|
||||
set_req = self.gateway.const.SetReq
|
||||
|
||||
if device_attr is not None:
|
||||
data.update(device_attr)
|
||||
for value_type, value in self._values.items():
|
||||
if value_type != self.value_type:
|
||||
try:
|
||||
attr[set_req(value_type).name] = value
|
||||
except ValueError:
|
||||
_LOGGER.error('value_type %s is not valid for mysensors '
|
||||
'version %s', value_type,
|
||||
self.gateway.version)
|
||||
return attr
|
||||
|
||||
return data
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self.value_type in self._values
|
||||
|
||||
def update(self):
|
||||
"""Update the controller with the latest values from a sensor."""
|
||||
|
|
|
@ -21,7 +21,7 @@ SENSOR_TYPES = ['humidity',
|
|||
'last_connection',
|
||||
'battery_level']
|
||||
|
||||
SENSOR_UNITS = {'humidity': '%', 'battery_level': '%'}
|
||||
SENSOR_UNITS = {'humidity': '%', 'battery_level': 'V'}
|
||||
|
||||
SENSOR_TEMP_TYPES = ['temperature',
|
||||
'target',
|
||||
|
|
|
@ -87,7 +87,7 @@ class RfxtrxSensor(Entity):
|
|||
return self._name
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def device_state_attributes(self):
|
||||
return self.event.values
|
||||
|
||||
@property
|
||||
|
|
|
@ -75,7 +75,7 @@ class SwissPublicTransportSensor(Entity):
|
|||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def device_state_attributes(self):
|
||||
""" Returns the state attributes. """
|
||||
if self._times is not None:
|
||||
return {
|
||||
|
|
|
@ -113,8 +113,8 @@ class TelldusLiveSensor(Entity):
|
|||
return self._value_as_humidity
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
attrs = dict()
|
||||
def device_state_attributes(self):
|
||||
attrs = {}
|
||||
if self._battery_level is not None:
|
||||
attrs[ATTR_BATTERY_LEVEL] = self._battery_level
|
||||
if self._last_updated is not None:
|
||||
|
|
|
@ -94,10 +94,7 @@ class SensorTemplate(Entity):
|
|||
|
||||
def _update_callback(_event):
|
||||
""" Called when the target device changes state. """
|
||||
# This can be called before the entity is properly
|
||||
# initialised, so check before updating state,
|
||||
if self.entity_id:
|
||||
self.update_ha_state(True)
|
||||
self.update_ha_state(True)
|
||||
|
||||
self.hass.bus.listen(EVENT_STATE_CHANGED, _update_callback)
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ class TwitchSensor(Entity):
|
|||
self._state = STATE_OFFLINE
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def device_state_attributes(self):
|
||||
""" Returns the state attributes. """
|
||||
if self._state == STATE_STREAMING:
|
||||
return {
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME,
|
||||
TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
REQUIREMENTS = ['pyvera==0.2.7']
|
||||
REQUIREMENTS = ['pyvera==0.2.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -118,7 +118,7 @@ class VeraSensor(Entity):
|
|||
return '%'
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def device_state_attributes(self):
|
||||
attr = {}
|
||||
if self.vera_device.has_battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
||||
|
|
|
@ -11,7 +11,7 @@ import logging
|
|||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.4.2']
|
||||
REQUIREMENTS = ['python-wink==0.5.0']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
|
|
@ -100,7 +100,7 @@ class YrSensor(Entity):
|
|||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def device_state_attributes(self):
|
||||
""" Returns state attributes. """
|
||||
data = {
|
||||
'about': "Weather forecast from yr.no, delivered by the"
|
||||
|
|
|
@ -111,11 +111,6 @@ class ZWaveSensor(ZWaveDeviceEntity, Entity):
|
|||
""" Returns the state of the sensor. """
|
||||
return self._value.data
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return self.device_state_attributes
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
return self._value.units
|
||||
|
|
|
@ -3,6 +3,9 @@ homeassistant.components.splunk
|
|||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Splunk component which allows you to send data to an Splunk instance
|
||||
utilizing the HTTP Event Collector.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/splunk/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
|
|
@ -129,11 +129,6 @@ class SwitchDevice(ToggleEntity):
|
|||
""" Is the device in standby. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Returns device specific state attributes. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
|
@ -144,9 +139,4 @@ class SwitchDevice(ToggleEntity):
|
|||
if value:
|
||||
data[attr] = value
|
||||
|
||||
device_attr = self.device_state_attributes
|
||||
|
||||
if device_attr is not None:
|
||||
data.update(device_attr)
|
||||
|
||||
return data
|
||||
|
|
|
@ -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
|
|
@ -100,28 +100,24 @@ class MySensorsSwitch(SwitchDevice):
|
|||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
device_attr = {}
|
||||
for value_type, value in self._values.items():
|
||||
if value_type != self.value_type:
|
||||
device_attr[self.gateway.const.SetReq(value_type).name] = value
|
||||
return device_attr
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
data = {
|
||||
attr = {
|
||||
mysensors.ATTR_PORT: self.gateway.port,
|
||||
mysensors.ATTR_NODE_ID: self.node_id,
|
||||
mysensors.ATTR_CHILD_ID: self.child_id,
|
||||
ATTR_BATTERY_LEVEL: self.battery_level,
|
||||
}
|
||||
|
||||
device_attr = self.device_state_attributes
|
||||
set_req = self.gateway.const.SetReq
|
||||
|
||||
if device_attr is not None:
|
||||
data.update(device_attr)
|
||||
|
||||
return data
|
||||
for value_type, value in self._values.items():
|
||||
if value_type != self.value_type:
|
||||
try:
|
||||
attr[set_req(value_type).name] = value
|
||||
except ValueError:
|
||||
_LOGGER.error('value_type %s is not valid for mysensors '
|
||||
'version %s', value_type,
|
||||
self.gateway.version)
|
||||
return attr
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
@ -144,6 +140,11 @@ class MySensorsSwitch(SwitchDevice):
|
|||
self._values[self.value_type] = STATE_OFF
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self.value_type in self._values
|
||||
|
||||
def update(self):
|
||||
"""Update the controller with the latest value from a sensor."""
|
||||
node = self.gateway.sensors[self.node_id]
|
||||
|
|
|
@ -8,8 +8,7 @@ https://home-assistant.io/components/switch.tellstick/
|
|||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
|
||||
ATTR_FRIENDLY_NAME)
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
SIGNAL_REPETITIONS = 1
|
||||
|
@ -63,7 +62,6 @@ class TellstickSwitchDevice(ToggleEntity):
|
|||
import tellcore.constants as tellcore_constants
|
||||
|
||||
self.tellstick_device = tellstick_device
|
||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
|
||||
self.signal_repetitions = signal_repetitions
|
||||
|
||||
self.last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
|
||||
|
@ -79,11 +77,6 @@ class TellstickSwitchDevice(ToggleEntity):
|
|||
""" Returns the name of the switch if any. """
|
||||
return self.tellstick_device.name
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return self.state_attr
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if switch is on. """
|
||||
|
|
|
@ -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)
|
|
@ -21,7 +21,7 @@ from homeassistant.const import (
|
|||
STATE_ON,
|
||||
STATE_OFF)
|
||||
|
||||
REQUIREMENTS = ['pyvera==0.2.7']
|
||||
REQUIREMENTS = ['pyvera==0.2.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -102,8 +102,8 @@ class VeraSwitch(SwitchDevice):
|
|||
return self._name
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
attr = super().state_attributes or {}
|
||||
def device_state_attributes(self):
|
||||
attr = {}
|
||||
|
||||
if self.vera_device.has_battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
||||
|
|
|
@ -12,7 +12,7 @@ from homeassistant.components.switch import SwitchDevice
|
|||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
REQUIREMENTS = ['pywemo==0.3.8']
|
||||
REQUIREMENTS = ['pywemo==0.3.9']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_WEMO_SUBSCRIPTION_REGISTRY = None
|
||||
|
@ -95,8 +95,8 @@ class WemoSwitch(SwitchDevice):
|
|||
return self.wemo.name
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
attr = super().state_attributes or {}
|
||||
def device_state_attributes(self):
|
||||
attr = {}
|
||||
if self.maker_params:
|
||||
# Is the maker sensor on or off.
|
||||
if self.maker_params['hassensor']:
|
||||
|
@ -153,6 +153,18 @@ class WemoSwitch(SwitchDevice):
|
|||
""" True if switch is on. """
|
||||
return self.wemo.get_state()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
""" True if switch is available. """
|
||||
if (self.wemo.model_name == 'Insight' and
|
||||
self.insight_params is None):
|
||||
return False
|
||||
|
||||
if (self.wemo.model_name == 'Maker' and
|
||||
self.maker_params is None):
|
||||
return False
|
||||
return True
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
self.wemo.on()
|
||||
|
|
|
@ -11,7 +11,7 @@ import logging
|
|||
from homeassistant.components.wink import WinkToggleDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.4.2']
|
||||
REQUIREMENTS = ['python-wink==0.5.0']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
|
|
@ -178,11 +178,6 @@ class ThermostatDevice(Entity):
|
|||
""" Returns the current state. """
|
||||
return self.target_temperature or STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Returns device specific state attributes. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
|
@ -211,11 +206,6 @@ class ThermostatDevice(Entity):
|
|||
if is_fan_on is not None:
|
||||
data[ATTR_FAN] = STATE_ON if is_fan_on else STATE_OFF
|
||||
|
||||
device_attr = self.device_state_attributes
|
||||
|
||||
if device_attr is not None:
|
||||
data.update(device_attr)
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
|
|
|
@ -9,33 +9,32 @@ https://home-assistant.io/components/thermostat.honeywell/
|
|||
import logging
|
||||
import socket
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.thermostat import ThermostatDevice
|
||||
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS)
|
||||
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS,
|
||||
TEMP_FAHRENHEIT)
|
||||
|
||||
REQUIREMENTS = ['evohomeclient==0.2.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AWAY_TEMP = "away_temperature"
|
||||
US_SYSTEM_SWITCH_POSITIONS = {1: 'Heat',
|
||||
2: 'Off',
|
||||
3: 'Cool'}
|
||||
US_BASEURL = 'https://mytotalconnectcomfort.com/portal'
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the honeywel thermostat. """
|
||||
def _setup_round(username, password, config, add_devices):
|
||||
from evohomeclient import EvohomeClient
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
try:
|
||||
away_temp = float(config.get(CONF_AWAY_TEMP, 16))
|
||||
except ValueError:
|
||||
_LOGGER.error("value entered for item %s should convert to a number",
|
||||
CONF_AWAY_TEMP)
|
||||
return False
|
||||
if username is None or password is None:
|
||||
_LOGGER.error("Missing required configuration items %s or %s",
|
||||
CONF_USERNAME, CONF_PASSWORD)
|
||||
return False
|
||||
|
||||
evo_api = EvohomeClient(username, password)
|
||||
|
||||
|
@ -53,6 +52,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
return False
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the honeywel thermostat. """
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
thermostat_id = config.get('id')
|
||||
|
||||
if username is None or password is None:
|
||||
_LOGGER.error("Missing required configuration items %s or %s",
|
||||
CONF_USERNAME, CONF_PASSWORD)
|
||||
return False
|
||||
|
||||
if thermostat_id:
|
||||
add_devices([HoneywellUSThermostat(thermostat_id, username, password)])
|
||||
else:
|
||||
return _setup_round(username, password, config, add_devices)
|
||||
|
||||
|
||||
class RoundThermostat(ThermostatDevice):
|
||||
""" Represents a Honeywell Round Connected thermostat. """
|
||||
|
||||
|
@ -135,3 +152,117 @@ class RoundThermostat(ThermostatDevice):
|
|||
else:
|
||||
self._name = data['name']
|
||||
self._is_dhw = False
|
||||
|
||||
|
||||
class HoneywellUSThermostat(ThermostatDevice):
|
||||
""" Represents a Honeywell US Thermostat. """
|
||||
|
||||
def __init__(self, ident, username, password):
|
||||
self._ident = ident
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._session = requests.Session()
|
||||
# Maybe this should be configurable?
|
||||
self._timeout = 30
|
||||
# Yeah, really.
|
||||
self._session.headers['X-Requested-With'] = 'XMLHttpRequest'
|
||||
self._update()
|
||||
|
||||
def _login(self):
|
||||
self._session.get(US_BASEURL, timeout=self._timeout)
|
||||
params = {'UserName': self._username,
|
||||
'Password': self._password,
|
||||
'RememberMe': 'false',
|
||||
'timeOffset': 480}
|
||||
resp = self._session.post(US_BASEURL, params=params,
|
||||
timeout=self._timeout)
|
||||
if resp.status_code != 200:
|
||||
_LOGGER('Login failed for user %(user)s',
|
||||
dict(user=self._username))
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def _get_data(self):
|
||||
if not self._login():
|
||||
return
|
||||
url = '%s/Device/CheckDataSession/%s' % (US_BASEURL, self._ident)
|
||||
resp = self._session.get(url, timeout=self._timeout)
|
||||
if resp.status_code < 300:
|
||||
return resp.json()
|
||||
else:
|
||||
return {'error': resp.status_code}
|
||||
|
||||
def _set_data(self, data):
|
||||
if not self._login():
|
||||
return
|
||||
url = '%s/Device/SubmitControlScreenChanges' % US_BASEURL
|
||||
data['DeviceID'] = self._ident
|
||||
resp = self._session.post(url, data=data, timeout=self._timeout)
|
||||
if resp.status_code < 300:
|
||||
return resp.json()
|
||||
else:
|
||||
return {'error': resp.status_code}
|
||||
|
||||
def _update(self):
|
||||
self._data = self._get_data()['latestData']
|
||||
|
||||
@property
|
||||
def is_fan_on(self):
|
||||
return self._data['fanData']['fanIsRunning']
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return 'honeywell'
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
unit = self._data['uiData']['DisplayUnits']
|
||||
if unit == 'F':
|
||||
return TEMP_FAHRENHEIT
|
||||
else:
|
||||
return TEMP_CELCIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
self._update()
|
||||
return self._data['uiData']['DispTemperature']
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
setpoint = US_SYSTEM_SWITCH_POSITIONS.get(
|
||||
self._data['uiData']['SystemSwitchPosition'],
|
||||
'Off')
|
||||
return self._data['uiData']['%sSetpoint' % setpoint]
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
""" Set target temperature. """
|
||||
data = {'SystemSwitch': None,
|
||||
'HeatSetpoint': None,
|
||||
'CoolSetpoint': None,
|
||||
'HeatNextPeriod': None,
|
||||
'CoolNextPeriod': None,
|
||||
'StatusHeat': None,
|
||||
'StatusCool': None,
|
||||
'FanMode': None}
|
||||
setpoint = US_SYSTEM_SWITCH_POSITIONS.get(
|
||||
self._data['uiData']['SystemSwitchPosition'],
|
||||
'Off')
|
||||
data['%sSetpoint' % setpoint] = temperature
|
||||
self._set_data(data)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Return device specific state attributes. """
|
||||
fanmodes = {0: "auto",
|
||||
1: "on",
|
||||
2: "circulate"}
|
||||
return {"fan": (self._data['fanData']['fanIsRunning'] and
|
||||
'running' or 'idle'),
|
||||
"fanmode": fanmodes[self._data['fanData']['fanMode']]}
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
pass
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
pass
|
||||
|
|
|
@ -26,6 +26,7 @@ DOMAIN = "verisure"
|
|||
DISCOVER_SENSORS = 'verisure.sensors'
|
||||
DISCOVER_SWITCHES = 'verisure.switches'
|
||||
DISCOVER_ALARMS = 'verisure.alarm_control_panel'
|
||||
DISCOVER_LOCKS = 'verisure.lock'
|
||||
|
||||
DEPENDENCIES = ['alarm_control_panel']
|
||||
REQUIREMENTS = ['vsure==0.5.0']
|
||||
|
@ -36,6 +37,7 @@ MY_PAGES = None
|
|||
ALARM_STATUS = {}
|
||||
SMARTPLUG_STATUS = {}
|
||||
CLIMATE_STATUS = {}
|
||||
LOCK_STATUS = {}
|
||||
|
||||
VERISURE_LOGIN_ERROR = None
|
||||
VERISURE_ERROR = None
|
||||
|
@ -44,6 +46,7 @@ SHOW_THERMOMETERS = True
|
|||
SHOW_HYGROMETERS = True
|
||||
SHOW_ALARM = True
|
||||
SHOW_SMARTPLUGS = True
|
||||
SHOW_LOCKS = True
|
||||
CODE_DIGITS = 4
|
||||
|
||||
# if wrong password was given don't try again
|
||||
|
@ -63,11 +66,12 @@ def setup(hass, config):
|
|||
from verisure import MyPages, LoginError, Error
|
||||
|
||||
global SHOW_THERMOMETERS, SHOW_HYGROMETERS,\
|
||||
SHOW_ALARM, SHOW_SMARTPLUGS, CODE_DIGITS
|
||||
SHOW_ALARM, SHOW_SMARTPLUGS, SHOW_LOCKS, CODE_DIGITS
|
||||
SHOW_THERMOMETERS = int(config[DOMAIN].get('thermometers', '1'))
|
||||
SHOW_HYGROMETERS = int(config[DOMAIN].get('hygrometers', '1'))
|
||||
SHOW_ALARM = int(config[DOMAIN].get('alarm', '1'))
|
||||
SHOW_SMARTPLUGS = int(config[DOMAIN].get('smartplugs', '1'))
|
||||
SHOW_LOCKS = int(config[DOMAIN].get('locks', '1'))
|
||||
CODE_DIGITS = int(config[DOMAIN].get('code_digits', '4'))
|
||||
|
||||
global MY_PAGES
|
||||
|
@ -87,11 +91,13 @@ def setup(hass, config):
|
|||
update_alarm()
|
||||
update_climate()
|
||||
update_smartplug()
|
||||
update_lock()
|
||||
|
||||
# Load components for the devices in the ISY controller that we support
|
||||
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
|
||||
('switch', DISCOVER_SWITCHES),
|
||||
('alarm_control_panel', DISCOVER_ALARMS))):
|
||||
('alarm_control_panel', DISCOVER_ALARMS),
|
||||
('lock', DISCOVER_LOCKS))):
|
||||
component = get_component(comp_name)
|
||||
_LOGGER.info(config[DOMAIN])
|
||||
bootstrap.setup_component(hass, component.DOMAIN, config)
|
||||
|
@ -134,6 +140,11 @@ def update_smartplug():
|
|||
update_component(MY_PAGES.smartplug.get, SMARTPLUG_STATUS)
|
||||
|
||||
|
||||
def update_lock():
|
||||
""" Updates the status of alarms. """
|
||||
update_component(MY_PAGES.lock.get, LOCK_STATUS)
|
||||
|
||||
|
||||
def update_component(get_function, status):
|
||||
""" Updates the status of verisure components. """
|
||||
if WRONG_PASSWORD_GIVEN:
|
||||
|
|
|
@ -13,10 +13,10 @@ from homeassistant.helpers import validate_config
|
|||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.const import (
|
||||
EVENT_PLATFORM_DISCOVERED, CONF_ACCESS_TOKEN,
|
||||
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME)
|
||||
ATTR_SERVICE, ATTR_DISCOVERED)
|
||||
|
||||
DOMAIN = "wink"
|
||||
REQUIREMENTS = ['python-wink==0.4.2']
|
||||
REQUIREMENTS = ['python-wink==0.5.0']
|
||||
|
||||
DISCOVER_LIGHTS = "wink.lights"
|
||||
DISCOVER_SWITCHES = "wink.switches"
|
||||
|
@ -82,13 +82,6 @@ class WinkToggleDevice(ToggleEntity):
|
|||
""" True if light is on. """
|
||||
return self.wink.state()
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return {
|
||||
ATTR_FRIENDLY_NAME: self.wink.name()
|
||||
}
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
self.wink.set_state(True)
|
||||
|
|
|
@ -200,3 +200,6 @@ HTTP_HEADER_EXPIRES = "Expires"
|
|||
CONTENT_TYPE_JSON = "application/json"
|
||||
CONTENT_TYPE_MULTIPART = 'multipart/x-mixed-replace; boundary={}'
|
||||
CONTENT_TYPE_TEXT_PLAIN = 'text/plain'
|
||||
|
||||
# The exit code to send to request a restart
|
||||
RESTART_EXIT_CODE = 100
|
||||
|
|
|
@ -10,9 +10,9 @@ import time
|
|||
import logging
|
||||
import signal
|
||||
import threading
|
||||
from types import MappingProxyType
|
||||
import enum
|
||||
import functools as ft
|
||||
from collections import namedtuple
|
||||
|
||||
from homeassistant.const import (
|
||||
__version__, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
|
@ -20,7 +20,8 @@ from homeassistant.const import (
|
|||
EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
|
||||
EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL,
|
||||
EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED,
|
||||
TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME, ATTR_SERVICE_DATA)
|
||||
TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME, ATTR_SERVICE_DATA,
|
||||
RESTART_EXIT_CODE)
|
||||
from homeassistant.exceptions import (
|
||||
HomeAssistantError, InvalidEntityFormatError)
|
||||
import homeassistant.util as util
|
||||
|
@ -45,12 +46,6 @@ MIN_WORKER_THREAD = 2
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Temporary to support deprecated methods
|
||||
_MockHA = namedtuple("MockHomeAssistant", ['bus'])
|
||||
|
||||
# The exit code to send to request a restart
|
||||
RESTART_EXIT_CODE = 100
|
||||
|
||||
|
||||
class HomeAssistant(object):
|
||||
"""Root object of the Home Assistant home automation."""
|
||||
|
@ -97,7 +92,10 @@ class HomeAssistant(object):
|
|||
'Could not bind to SIGTERM. Are you running in a thread?')
|
||||
|
||||
while not request_shutdown.isSet():
|
||||
time.sleep(1)
|
||||
try:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
self.stop()
|
||||
return RESTART_EXIT_CODE if request_restart.isSet() else 0
|
||||
|
@ -113,46 +111,6 @@ class HomeAssistant(object):
|
|||
|
||||
self.pool.stop()
|
||||
|
||||
def track_point_in_time(self, action, point_in_time):
|
||||
"""Deprecated method as of 8/4/2015 to track point in time."""
|
||||
_LOGGER.warning(
|
||||
'hass.track_point_in_time is deprecated. '
|
||||
'Please use homeassistant.helpers.event.track_point_in_time')
|
||||
import homeassistant.helpers.event as helper
|
||||
helper.track_point_in_time(self, action, point_in_time)
|
||||
|
||||
def track_point_in_utc_time(self, action, point_in_time):
|
||||
"""Deprecated method as of 8/4/2015 to track point in UTC time."""
|
||||
_LOGGER.warning(
|
||||
'hass.track_point_in_utc_time is deprecated. '
|
||||
'Please use homeassistant.helpers.event.track_point_in_utc_time')
|
||||
import homeassistant.helpers.event as helper
|
||||
helper.track_point_in_utc_time(self, action, point_in_time)
|
||||
|
||||
def track_utc_time_change(self, action,
|
||||
year=None, month=None, day=None,
|
||||
hour=None, minute=None, second=None):
|
||||
"""Deprecated method as of 8/4/2015 to track UTC time change."""
|
||||
# pylint: disable=too-many-arguments
|
||||
_LOGGER.warning(
|
||||
'hass.track_utc_time_change is deprecated. '
|
||||
'Please use homeassistant.helpers.event.track_utc_time_change')
|
||||
import homeassistant.helpers.event as helper
|
||||
helper.track_utc_time_change(self, action, year, month, day, hour,
|
||||
minute, second)
|
||||
|
||||
def track_time_change(self, action,
|
||||
year=None, month=None, day=None,
|
||||
hour=None, minute=None, second=None, utc=False):
|
||||
"""Deprecated method as of 8/4/2015 to track time change."""
|
||||
# pylint: disable=too-many-arguments
|
||||
_LOGGER.warning(
|
||||
'hass.track_time_change is deprecated. '
|
||||
'Please use homeassistant.helpers.event.track_time_change')
|
||||
import homeassistant.helpers.event as helper
|
||||
helper.track_time_change(self, action, year, month, day, hour,
|
||||
minute, second)
|
||||
|
||||
|
||||
class JobPriority(util.OrderedEnum):
|
||||
"""Provides job priorities for event bus jobs."""
|
||||
|
@ -352,7 +310,7 @@ class State(object):
|
|||
|
||||
self.entity_id = entity_id.lower()
|
||||
self.state = state
|
||||
self.attributes = attributes or {}
|
||||
self.attributes = MappingProxyType(attributes or {})
|
||||
self.last_updated = dt_util.strip_microseconds(
|
||||
last_updated or dt_util.utcnow())
|
||||
|
||||
|
@ -380,12 +338,6 @@ class State(object):
|
|||
self.attributes.get(ATTR_FRIENDLY_NAME) or
|
||||
self.object_id.replace('_', ' '))
|
||||
|
||||
def copy(self):
|
||||
"""Return a copy of the state."""
|
||||
return State(self.entity_id, self.state,
|
||||
dict(self.attributes), self.last_changed,
|
||||
self.last_updated)
|
||||
|
||||
def as_dict(self):
|
||||
"""Return a dict representation of the State.
|
||||
|
||||
|
@ -394,7 +346,7 @@ class State(object):
|
|||
"""
|
||||
return {'entity_id': self.entity_id,
|
||||
'state': self.state,
|
||||
'attributes': self.attributes,
|
||||
'attributes': dict(self.attributes),
|
||||
'last_changed': dt_util.datetime_to_str(self.last_changed),
|
||||
'last_updated': dt_util.datetime_to_str(self.last_updated)}
|
||||
|
||||
|
@ -458,14 +410,11 @@ class StateMachine(object):
|
|||
def all(self):
|
||||
"""Create a list of all states."""
|
||||
with self._lock:
|
||||
return [state.copy() for state in self._states.values()]
|
||||
return list(self._states.values())
|
||||
|
||||
def get(self, entity_id):
|
||||
"""Retrieve state of entity_id or None if not found."""
|
||||
state = self._states.get(entity_id.lower())
|
||||
|
||||
# Make a copy so people won't mutate the state
|
||||
return state.copy() if state else None
|
||||
return self._states.get(entity_id.lower())
|
||||
|
||||
def is_state(self, entity_id, state):
|
||||
"""Test if entity exists and is specified state."""
|
||||
|
@ -526,15 +475,6 @@ class StateMachine(object):
|
|||
|
||||
self._bus.fire(EVENT_STATE_CHANGED, event_data)
|
||||
|
||||
def track_change(self, entity_ids, action, from_state=None, to_state=None):
|
||||
"""DEPRECATED AS OF 8/4/2015."""
|
||||
_LOGGER.warning(
|
||||
'hass.states.track_change is deprecated. '
|
||||
'Use homeassistant.helpers.event.track_state_change instead.')
|
||||
import homeassistant.helpers.event as helper
|
||||
helper.track_state_change(_MockHA(self._bus), entity_ids, action,
|
||||
from_state, to_state)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class Service(object):
|
||||
|
|
|
@ -80,7 +80,20 @@ class Entity(object):
|
|||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
"""
|
||||
Return the state attributes.
|
||||
|
||||
Implemented by component base class.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""
|
||||
Return device specific state attributes.
|
||||
|
||||
Implemented by platform classes.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
|
@ -135,6 +148,11 @@ class Entity(object):
|
|||
state = str(self.state)
|
||||
attr = self.state_attributes or {}
|
||||
|
||||
device_attr = self.device_state_attributes
|
||||
|
||||
if device_attr is not None:
|
||||
attr.update(device_attr)
|
||||
|
||||
if ATTR_UNIT_OF_MEASUREMENT not in attr and \
|
||||
self.unit_of_measurement is not None:
|
||||
attr[ATTR_UNIT_OF_MEASUREMENT] = str(self.unit_of_measurement)
|
||||
|
|
|
@ -85,7 +85,7 @@ def reproduce_state(hass, states, blocking=False):
|
|||
# We group service calls for entities by service call
|
||||
# json used to create a hashable version of dict with maybe lists in it
|
||||
key = (service_domain, service,
|
||||
json.dumps(state.attributes, sort_keys=True))
|
||||
json.dumps(dict(state.attributes), sort_keys=True))
|
||||
to_call[key].append(state.entity_id)
|
||||
|
||||
for (service_domain, service, service_data), entity_ids in to_call.items():
|
||||
|
|
|
@ -15,6 +15,7 @@ import socket
|
|||
import random
|
||||
import string
|
||||
from functools import wraps
|
||||
from types import MappingProxyType
|
||||
|
||||
from .dt import datetime_to_local_str, utcnow
|
||||
|
||||
|
@ -42,7 +43,7 @@ def slugify(text):
|
|||
|
||||
def repr_helper(inp):
|
||||
""" Helps creating a more readable string representation of objects. """
|
||||
if isinstance(inp, dict):
|
||||
if isinstance(inp, (dict, MappingProxyType)):
|
||||
return ", ".join(
|
||||
repr_helper(key)+"="+repr_helper(item) for key, item
|
||||
in inp.items())
|
||||
|
|
|
@ -48,7 +48,7 @@ def as_utc(dattim):
|
|||
if dattim.tzinfo == UTC:
|
||||
return dattim
|
||||
elif dattim.tzinfo is None:
|
||||
dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE)
|
||||
dattim = DEFAULT_TIME_ZONE.localize(dattim)
|
||||
|
||||
return dattim.astimezone(UTC)
|
||||
|
||||
|
@ -58,7 +58,7 @@ def as_local(dattim):
|
|||
if dattim.tzinfo == DEFAULT_TIME_ZONE:
|
||||
return dattim
|
||||
elif dattim.tzinfo is None:
|
||||
dattim = dattim.replace(tzinfo=UTC)
|
||||
dattim = UTC.localize(dattim)
|
||||
|
||||
return dattim.astimezone(DEFAULT_TIME_ZONE)
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ https://github.com/theolind/pymysensors/archive/005bff4c5ca7a56acd30e816bc3bcdb5
|
|||
https://github.com/w1ll1am23/pygooglevoice-sms/archive/7c5ee9969b97a7992fc86a753fe9f20e3ffa3f7c.zip#pygooglevoice-sms==0.0.1
|
||||
|
||||
# homeassistant.components.influxdb
|
||||
influxdb==2.11.0
|
||||
influxdb==2.12.0
|
||||
|
||||
# homeassistant.components.insteon_hub
|
||||
insteon_hub==0.4.5
|
||||
|
@ -105,6 +105,10 @@ liffylights==0.9.4
|
|||
# homeassistant.components.light.limitlessled
|
||||
limitlessled==1.0.0
|
||||
|
||||
# homeassistant.components.sensor.mfi
|
||||
# homeassistant.components.switch.mfi
|
||||
mficlient==0.2.2
|
||||
|
||||
# homeassistant.components.discovery
|
||||
netdisco==0.5.2
|
||||
|
||||
|
@ -153,6 +157,9 @@ pyicloud==0.7.2
|
|||
# homeassistant.components.device_tracker.netgear
|
||||
pynetgear==0.3.2
|
||||
|
||||
# homeassistant.components.alarm_control_panel.nx584
|
||||
pynx584==0.1
|
||||
|
||||
# homeassistant.components.sensor.openweathermap
|
||||
pyowm==2.3.0
|
||||
|
||||
|
@ -183,13 +190,16 @@ python-telegram-bot==3.2.0
|
|||
# homeassistant.components.sensor.twitch
|
||||
python-twitch==1.2.0
|
||||
|
||||
# homeassistant.components.garage_door.wink
|
||||
python-wink==0.4.2
|
||||
|
||||
# homeassistant.components.wink
|
||||
# homeassistant.components.garage_door.wink
|
||||
# homeassistant.components.light.wink
|
||||
# homeassistant.components.lock.wink
|
||||
# homeassistant.components.sensor.wink
|
||||
# homeassistant.components.switch.wink
|
||||
python-wink==0.4.2
|
||||
python-wink==0.5.0
|
||||
|
||||
# homeassistant.components.keyboard
|
||||
pyuserinput==0.1.9
|
||||
|
@ -197,10 +207,10 @@ pyuserinput==0.1.9
|
|||
# homeassistant.components.light.vera
|
||||
# homeassistant.components.sensor.vera
|
||||
# homeassistant.components.switch.vera
|
||||
pyvera==0.2.7
|
||||
pyvera==0.2.8
|
||||
|
||||
# homeassistant.components.switch.wemo
|
||||
pywemo==0.3.8
|
||||
pywemo==0.3.9
|
||||
|
||||
# homeassistant.components.thermostat.radiotherm
|
||||
radiotherm==1.2
|
||||
|
@ -229,6 +239,9 @@ tellive-py==0.5.2
|
|||
# homeassistant.components.switch.transmission
|
||||
transmissionrpc==0.11
|
||||
|
||||
# homeassistant.components.camera.uvc
|
||||
uvcclient==0.5
|
||||
|
||||
# homeassistant.components.verisure
|
||||
vsure==0.5.0
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
# script/test: Run test suite for application. Optionallly pass in a path to an
|
||||
# script/test: Run test suite for application. Optionally pass in a path to an
|
||||
# individual test file to run a single test.
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
|
|
@ -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()
|
|
@ -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)
|
|
@ -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)
|
|
@ -236,3 +236,14 @@ class TestComponentsGroup(unittest.TestCase):
|
|||
grp2 = group.Group(self.hass, 'Je suis Charlie')
|
||||
|
||||
self.assertNotEqual(grp1.entity_id, grp2.entity_id)
|
||||
|
||||
def test_expand_entity_ids_expands_nested_groups(self):
|
||||
group.Group(self.hass, 'light', ['light.test_1', 'light.test_2'])
|
||||
group.Group(self.hass, 'switch', ['switch.test_1', 'switch.test_2'])
|
||||
group.Group(self.hass, 'group_of_groups', ['group.light',
|
||||
'group.switch'])
|
||||
|
||||
self.assertEqual(
|
||||
['light.test_1', 'light.test_2', 'switch.test_1', 'switch.test_2'],
|
||||
sorted(group.expand_entity_ids(self.hass,
|
||||
['group.group_of_groups'])))
|
||||
|
|
|
@ -68,7 +68,7 @@ class TestMQTT(unittest.TestCase):
|
|||
self.assertEqual('test-payload',
|
||||
self.calls[0][0].data['service_data'][mqtt.ATTR_PAYLOAD])
|
||||
|
||||
def test_service_call_without_topic_does_not_publush(self):
|
||||
def test_service_call_without_topic_does_not_publish(self):
|
||||
self.hass.bus.fire(EVENT_CALL_SERVICE, {
|
||||
ATTR_DOMAIN: mqtt.DOMAIN,
|
||||
ATTR_SERVICE: mqtt.SERVICE_PUBLISH
|
||||
|
@ -76,6 +76,42 @@ class TestMQTT(unittest.TestCase):
|
|||
self.hass.pool.block_till_done()
|
||||
self.assertTrue(not mqtt.MQTT_CLIENT.publish.called)
|
||||
|
||||
def test_service_call_with_template_payload_renders_template(self):
|
||||
"""
|
||||
If 'payload_template' is provided and 'payload' is not, then render it.
|
||||
"""
|
||||
mqtt.publish_template(self.hass, "test/topic", "{{ 1+1 }}")
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertTrue(mqtt.MQTT_CLIENT.publish.called)
|
||||
self.assertEqual(mqtt.MQTT_CLIENT.publish.call_args[0][1], "2")
|
||||
|
||||
def test_service_call_with_payload_doesnt_render_template(self):
|
||||
"""
|
||||
If a 'payload' is provided then use that instead of 'payload_template'.
|
||||
"""
|
||||
payload = "not a template"
|
||||
payload_template = "a template"
|
||||
# Call the service directly because the helper functions don't allow
|
||||
# you to provide payload AND payload_template.
|
||||
self.hass.services.call(mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, {
|
||||
mqtt.ATTR_TOPIC: "test/topic",
|
||||
mqtt.ATTR_PAYLOAD: payload,
|
||||
mqtt.ATTR_PAYLOAD_TEMPLATE: payload_template
|
||||
}, blocking=True)
|
||||
self.assertTrue(mqtt.MQTT_CLIENT.publish.called)
|
||||
self.assertEqual(mqtt.MQTT_CLIENT.publish.call_args[0][1], payload)
|
||||
|
||||
def test_service_call_without_payload_or_payload_template(self):
|
||||
"""
|
||||
If neither 'payload' or 'payload_template' is provided then fail.
|
||||
"""
|
||||
# Call the service directly because the helper functions require you to
|
||||
# provide a payload.
|
||||
self.hass.services.call(mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, {
|
||||
mqtt.ATTR_TOPIC: "test/topic"
|
||||
}, blocking=True)
|
||||
self.assertFalse(mqtt.MQTT_CLIENT.publish.called)
|
||||
|
||||
def test_subscribe_topic(self):
|
||||
mqtt.subscribe(self.hass, 'test-topic', self.record_calls)
|
||||
|
||||
|
|
|
@ -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'
|
|
@ -23,7 +23,7 @@ import homeassistant.util.dt as dt_util
|
|||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.const import (
|
||||
__version__, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
ATTR_FRIENDLY_NAME, TEMP_CELCIUS,
|
||||
EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, TEMP_CELCIUS,
|
||||
TEMP_FAHRENHEIT)
|
||||
|
||||
PST = pytz.timezone('America/Los_Angeles')
|
||||
|
@ -93,65 +93,6 @@ class TestHomeAssistant(unittest.TestCase):
|
|||
|
||||
self.assertEqual(1, len(calls))
|
||||
|
||||
def test_track_point_in_time(self):
|
||||
""" Test track point in time. """
|
||||
before_birthday = datetime(1985, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC)
|
||||
birthday_paulus = datetime(1986, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC)
|
||||
after_birthday = datetime(1987, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC)
|
||||
|
||||
runs = []
|
||||
|
||||
self.hass.track_point_in_utc_time(
|
||||
lambda x: runs.append(1), birthday_paulus)
|
||||
|
||||
self._send_time_changed(before_birthday)
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual(0, len(runs))
|
||||
|
||||
self._send_time_changed(birthday_paulus)
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual(1, len(runs))
|
||||
|
||||
# A point in time tracker will only fire once, this should do nothing
|
||||
self._send_time_changed(birthday_paulus)
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual(1, len(runs))
|
||||
|
||||
self.hass.track_point_in_time(
|
||||
lambda x: runs.append(1), birthday_paulus)
|
||||
|
||||
self._send_time_changed(after_birthday)
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual(2, len(runs))
|
||||
|
||||
def test_track_time_change(self):
|
||||
""" Test tracking time change. """
|
||||
wildcard_runs = []
|
||||
specific_runs = []
|
||||
|
||||
self.hass.track_time_change(lambda x: wildcard_runs.append(1))
|
||||
self.hass.track_utc_time_change(
|
||||
lambda x: specific_runs.append(1), second=[0, 30])
|
||||
|
||||
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0))
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual(1, len(specific_runs))
|
||||
self.assertEqual(1, len(wildcard_runs))
|
||||
|
||||
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15))
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual(1, len(specific_runs))
|
||||
self.assertEqual(2, len(wildcard_runs))
|
||||
|
||||
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30))
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual(2, len(specific_runs))
|
||||
self.assertEqual(3, len(wildcard_runs))
|
||||
|
||||
def _send_time_changed(self, now):
|
||||
""" Send a time changed event. """
|
||||
self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now})
|
||||
|
||||
|
||||
class TestEvent(unittest.TestCase):
|
||||
""" Test Event class. """
|
||||
|
@ -267,18 +208,6 @@ class TestState(unittest.TestCase):
|
|||
{ATTR_FRIENDLY_NAME: name})
|
||||
self.assertEqual(name, state.name)
|
||||
|
||||
def test_copy(self):
|
||||
state = ha.State('domain.hello', 'world', {'some': 'attr'})
|
||||
# Patch dt_util.utcnow() so we know last_updated got copied too
|
||||
with patch('homeassistant.core.dt_util.utcnow',
|
||||
return_value=dt_util.utcnow() + timedelta(seconds=10)):
|
||||
copy = state.copy()
|
||||
self.assertEqual(state.entity_id, copy.entity_id)
|
||||
self.assertEqual(state.state, copy.state)
|
||||
self.assertEqual(state.attributes, copy.attributes)
|
||||
self.assertEqual(state.last_changed, copy.last_changed)
|
||||
self.assertEqual(state.last_updated, copy.last_updated)
|
||||
|
||||
def test_dict_conversion(self):
|
||||
state = ha.State('domain.hello', 'world', {'some': 'attr'})
|
||||
self.assertEqual(state, ha.State.from_dict(state.as_dict()))
|
||||
|
@ -358,52 +287,11 @@ class TestStateMachine(unittest.TestCase):
|
|||
# If it does not exist, we should get False
|
||||
self.assertFalse(self.states.remove('light.Bowl'))
|
||||
|
||||
def test_track_change(self):
|
||||
""" Test states.track_change. """
|
||||
self.pool.add_worker()
|
||||
|
||||
# 2 lists to track how often our callbacks got called
|
||||
specific_runs = []
|
||||
wildcard_runs = []
|
||||
|
||||
self.states.track_change(
|
||||
'light.Bowl', lambda a, b, c: specific_runs.append(1), 'on', 'off')
|
||||
|
||||
self.states.track_change(
|
||||
'light.Bowl', lambda a, b, c: wildcard_runs.append(1),
|
||||
ha.MATCH_ALL, ha.MATCH_ALL)
|
||||
|
||||
# Set same state should not trigger a state change/listener
|
||||
self.states.set('light.Bowl', 'on')
|
||||
self.bus._pool.block_till_done()
|
||||
self.assertEqual(0, len(specific_runs))
|
||||
self.assertEqual(0, len(wildcard_runs))
|
||||
|
||||
# State change off -> on
|
||||
self.states.set('light.Bowl', 'off')
|
||||
self.bus._pool.block_till_done()
|
||||
self.assertEqual(1, len(specific_runs))
|
||||
self.assertEqual(1, len(wildcard_runs))
|
||||
|
||||
# State change off -> off
|
||||
self.states.set('light.Bowl', 'off', {"some_attr": 1})
|
||||
self.bus._pool.block_till_done()
|
||||
self.assertEqual(1, len(specific_runs))
|
||||
self.assertEqual(2, len(wildcard_runs))
|
||||
|
||||
# State change off -> on
|
||||
self.states.set('light.Bowl', 'on')
|
||||
self.bus._pool.block_till_done()
|
||||
self.assertEqual(1, len(specific_runs))
|
||||
self.assertEqual(3, len(wildcard_runs))
|
||||
|
||||
def test_case_insensitivty(self):
|
||||
self.pool.add_worker()
|
||||
runs = []
|
||||
|
||||
track_state_change(
|
||||
ha._MockHA(self.bus), 'light.BoWl', lambda a, b, c: runs.append(1),
|
||||
ha.MATCH_ALL, ha.MATCH_ALL)
|
||||
self.bus.listen(EVENT_STATE_CHANGED, lambda event: runs.append(event))
|
||||
|
||||
self.states.set('light.BOWL', 'off')
|
||||
self.bus._pool.block_till_done()
|
||||
|
|
Loading…
Reference in New Issue