mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
commit
e9fa1f1f83
20
.coveragerc
20
.coveragerc
@ -6,10 +6,17 @@ omit =
|
|||||||
|
|
||||||
# omit pieces of code that rely on external devices being present
|
# omit pieces of code that rely on external devices being present
|
||||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||||
|
homeassistant/components/alarm_control_panel/nx584.py
|
||||||
|
|
||||||
homeassistant/components/arduino.py
|
homeassistant/components/arduino.py
|
||||||
homeassistant/components/*/arduino.py
|
homeassistant/components/*/arduino.py
|
||||||
|
|
||||||
|
homeassistant/components/apcupsd.py
|
||||||
|
homeassistant/components/*/apcupsd.py
|
||||||
|
|
||||||
|
homeassistant/components/bloomsky.py
|
||||||
|
homeassistant/components/*/bloomsky.py
|
||||||
|
|
||||||
homeassistant/components/insteon_hub.py
|
homeassistant/components/insteon_hub.py
|
||||||
homeassistant/components/*/insteon_hub.py
|
homeassistant/components/*/insteon_hub.py
|
||||||
|
|
||||||
@ -53,6 +60,9 @@ omit =
|
|||||||
homeassistant/components/rpi_gpio.py
|
homeassistant/components/rpi_gpio.py
|
||||||
homeassistant/components/*/rpi_gpio.py
|
homeassistant/components/*/rpi_gpio.py
|
||||||
|
|
||||||
|
homeassistant/components/scsgate.py
|
||||||
|
homeassistant/components/*/scsgate.py
|
||||||
|
|
||||||
homeassistant/components/binary_sensor/arest.py
|
homeassistant/components/binary_sensor/arest.py
|
||||||
homeassistant/components/binary_sensor/rest.py
|
homeassistant/components/binary_sensor/rest.py
|
||||||
homeassistant/components/browser.py
|
homeassistant/components/browser.py
|
||||||
@ -73,9 +83,8 @@ omit =
|
|||||||
homeassistant/components/device_tracker/ubus.py
|
homeassistant/components/device_tracker/ubus.py
|
||||||
homeassistant/components/discovery.py
|
homeassistant/components/discovery.py
|
||||||
homeassistant/components/downloader.py
|
homeassistant/components/downloader.py
|
||||||
|
homeassistant/components/garage_door/wink.py
|
||||||
homeassistant/components/ifttt.py
|
homeassistant/components/ifttt.py
|
||||||
homeassistant/components/statsd.py
|
|
||||||
homeassistant/components/influxdb.py
|
|
||||||
homeassistant/components/keyboard.py
|
homeassistant/components/keyboard.py
|
||||||
homeassistant/components/light/blinksticklight.py
|
homeassistant/components/light/blinksticklight.py
|
||||||
homeassistant/components/light/hue.py
|
homeassistant/components/light/hue.py
|
||||||
@ -89,21 +98,24 @@ omit =
|
|||||||
homeassistant/components/media_player/kodi.py
|
homeassistant/components/media_player/kodi.py
|
||||||
homeassistant/components/media_player/mpd.py
|
homeassistant/components/media_player/mpd.py
|
||||||
homeassistant/components/media_player/plex.py
|
homeassistant/components/media_player/plex.py
|
||||||
|
homeassistant/components/media_player/samsungtv.py
|
||||||
|
homeassistant/components/media_player/snapcast.py
|
||||||
homeassistant/components/media_player/sonos.py
|
homeassistant/components/media_player/sonos.py
|
||||||
homeassistant/components/media_player/squeezebox.py
|
homeassistant/components/media_player/squeezebox.py
|
||||||
homeassistant/components/notify/free_mobile.py
|
homeassistant/components/notify/free_mobile.py
|
||||||
|
homeassistant/components/notify/googlevoice.py
|
||||||
homeassistant/components/notify/instapush.py
|
homeassistant/components/notify/instapush.py
|
||||||
homeassistant/components/notify/nma.py
|
homeassistant/components/notify/nma.py
|
||||||
homeassistant/components/notify/pushbullet.py
|
homeassistant/components/notify/pushbullet.py
|
||||||
homeassistant/components/notify/pushetta.py
|
homeassistant/components/notify/pushetta.py
|
||||||
homeassistant/components/notify/pushover.py
|
homeassistant/components/notify/pushover.py
|
||||||
|
homeassistant/components/notify/rest.py
|
||||||
homeassistant/components/notify/slack.py
|
homeassistant/components/notify/slack.py
|
||||||
homeassistant/components/notify/smtp.py
|
homeassistant/components/notify/smtp.py
|
||||||
homeassistant/components/notify/syslog.py
|
homeassistant/components/notify/syslog.py
|
||||||
homeassistant/components/notify/telegram.py
|
homeassistant/components/notify/telegram.py
|
||||||
homeassistant/components/notify/twitter.py
|
homeassistant/components/notify/twitter.py
|
||||||
homeassistant/components/notify/xmpp.py
|
homeassistant/components/notify/xmpp.py
|
||||||
homeassistant/components/notify/googlevoice.py
|
|
||||||
homeassistant/components/sensor/arest.py
|
homeassistant/components/sensor/arest.py
|
||||||
homeassistant/components/sensor/bitcoin.py
|
homeassistant/components/sensor/bitcoin.py
|
||||||
homeassistant/components/sensor/cpuspeed.py
|
homeassistant/components/sensor/cpuspeed.py
|
||||||
@ -118,6 +130,7 @@ omit =
|
|||||||
homeassistant/components/sensor/openweathermap.py
|
homeassistant/components/sensor/openweathermap.py
|
||||||
homeassistant/components/sensor/rest.py
|
homeassistant/components/sensor/rest.py
|
||||||
homeassistant/components/sensor/sabnzbd.py
|
homeassistant/components/sensor/sabnzbd.py
|
||||||
|
homeassistant/components/sensor/speedtest.py
|
||||||
homeassistant/components/sensor/swiss_public_transport.py
|
homeassistant/components/sensor/swiss_public_transport.py
|
||||||
homeassistant/components/sensor/systemmonitor.py
|
homeassistant/components/sensor/systemmonitor.py
|
||||||
homeassistant/components/sensor/temper.py
|
homeassistant/components/sensor/temper.py
|
||||||
@ -140,7 +153,6 @@ omit =
|
|||||||
homeassistant/components/thermostat/proliphix.py
|
homeassistant/components/thermostat/proliphix.py
|
||||||
homeassistant/components/thermostat/radiotherm.py
|
homeassistant/components/thermostat/radiotherm.py
|
||||||
|
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
# Regexes for lines to exclude from consideration
|
# Regexes for lines to exclude from consideration
|
||||||
exclude_lines =
|
exclude_lines =
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
""" Starts home assistant. """
|
""" Starts home assistant. """
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
|
from multiprocessing import Process
|
||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
|
import time
|
||||||
|
|
||||||
from homeassistant import bootstrap
|
from homeassistant import bootstrap
|
||||||
import homeassistant.config as config_util
|
import homeassistant.config as config_util
|
||||||
from homeassistant.const import __version__, EVENT_HOMEASSISTANT_START
|
from homeassistant.const import (__version__, EVENT_HOMEASSISTANT_START,
|
||||||
|
RESTART_EXIT_CODE)
|
||||||
|
|
||||||
|
|
||||||
def validate_python():
|
def validate_python():
|
||||||
@ -73,6 +78,11 @@ def get_arguments():
|
|||||||
'--demo-mode',
|
'--demo-mode',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help='Start Home Assistant in demo mode')
|
help='Start Home Assistant in demo mode')
|
||||||
|
parser.add_argument(
|
||||||
|
'--debug',
|
||||||
|
action='store_true',
|
||||||
|
help='Start Home Assistant in debug mode. Runs in single process to '
|
||||||
|
'enable use of interactive debuggers.')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--open-ui',
|
'--open-ui',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
@ -204,35 +214,11 @@ def uninstall_osx():
|
|||||||
print("Home Assistant has been uninstalled.")
|
print("Home Assistant has been uninstalled.")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def setup_and_run_hass(config_dir, args, top_process=False):
|
||||||
""" Starts Home Assistant. """
|
"""
|
||||||
validate_python()
|
Setup HASS and run. Block until stopped. Will assume it is running in a
|
||||||
|
subprocess unless top_process is set to true.
|
||||||
args = get_arguments()
|
"""
|
||||||
|
|
||||||
config_dir = os.path.join(os.getcwd(), args.config)
|
|
||||||
ensure_config_path(config_dir)
|
|
||||||
|
|
||||||
# os x launchd functions
|
|
||||||
if args.install_osx:
|
|
||||||
install_osx()
|
|
||||||
return
|
|
||||||
if args.uninstall_osx:
|
|
||||||
uninstall_osx()
|
|
||||||
return
|
|
||||||
if args.restart_osx:
|
|
||||||
uninstall_osx()
|
|
||||||
install_osx()
|
|
||||||
return
|
|
||||||
|
|
||||||
# daemon functions
|
|
||||||
if args.pid_file:
|
|
||||||
check_pid(args.pid_file)
|
|
||||||
if args.daemon:
|
|
||||||
daemonize()
|
|
||||||
if args.pid_file:
|
|
||||||
write_pid(args.pid_file)
|
|
||||||
|
|
||||||
if args.demo_mode:
|
if args.demo_mode:
|
||||||
config = {
|
config = {
|
||||||
'frontend': {},
|
'frontend': {},
|
||||||
@ -259,7 +245,91 @@ def main():
|
|||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
|
||||||
|
|
||||||
hass.start()
|
hass.start()
|
||||||
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):
|
||||||
|
""" Runs a child hass process. Returns True if it should be restarted. """
|
||||||
|
requested_stop = threading.Event()
|
||||||
|
hass_proc.daemon = True
|
||||||
|
|
||||||
|
def request_stop(*args):
|
||||||
|
""" request hass stop, *args is for signal handler callback """
|
||||||
|
requested_stop.set()
|
||||||
|
hass_proc.terminate()
|
||||||
|
|
||||||
|
try:
|
||||||
|
signal.signal(signal.SIGTERM, request_stop)
|
||||||
|
except ValueError:
|
||||||
|
print('Could not bind to SIGTERM. Are you running in a thread?')
|
||||||
|
|
||||||
|
hass_proc.start()
|
||||||
|
try:
|
||||||
|
hass_proc.join()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
request_stop()
|
||||||
|
try:
|
||||||
|
hass_proc.join()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return (not requested_stop.isSet() and
|
||||||
|
hass_proc.exitcode == RESTART_EXIT_CODE,
|
||||||
|
hass_proc.exitcode)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
""" Starts Home Assistant. """
|
||||||
|
validate_python()
|
||||||
|
|
||||||
|
args = get_arguments()
|
||||||
|
|
||||||
|
config_dir = os.path.join(os.getcwd(), args.config)
|
||||||
|
ensure_config_path(config_dir)
|
||||||
|
|
||||||
|
# os x launchd functions
|
||||||
|
if args.install_osx:
|
||||||
|
install_osx()
|
||||||
|
return 0
|
||||||
|
if args.uninstall_osx:
|
||||||
|
uninstall_osx()
|
||||||
|
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 0
|
||||||
|
|
||||||
|
# daemon functions
|
||||||
|
if args.pid_file:
|
||||||
|
check_pid(args.pid_file)
|
||||||
|
if args.daemon:
|
||||||
|
daemonize()
|
||||||
|
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, exit_code = run_hass_process(hass_proc)
|
||||||
|
return exit_code
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
sys.exit(main())
|
||||||
|
@ -61,6 +61,8 @@ def setup(hass, config):
|
|||||||
|
|
||||||
for alarm in target_alarms:
|
for alarm in target_alarms:
|
||||||
getattr(alarm, method)(code)
|
getattr(alarm, method)(code)
|
||||||
|
if alarm.should_poll:
|
||||||
|
alarm.update_ha_state(True)
|
||||||
|
|
||||||
descriptions = load_yaml_config_file(
|
descriptions = load_yaml_config_file(
|
||||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
@ -57,10 +57,12 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
|
""" No polling needed. """
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
""" Returns the name of the device. """
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -88,7 +90,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||||||
# Open another session to alarm.com to fire off the command
|
# Open another session to alarm.com to fire off the command
|
||||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||||
_alarm.disarm()
|
_alarm.disarm()
|
||||||
self.update_ha_state()
|
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
""" Send arm home command. """
|
""" Send arm home command. """
|
||||||
@ -98,7 +99,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||||||
# Open another session to alarm.com to fire off the command
|
# Open another session to alarm.com to fire off the command
|
||||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||||
_alarm.arm_stay()
|
_alarm.arm_stay()
|
||||||
self.update_ha_state()
|
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
""" Send arm away command. """
|
""" Send arm away command. """
|
||||||
@ -108,7 +108,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||||||
# Open another session to alarm.com to fire off the command
|
# Open another session to alarm.com to fire off the command
|
||||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||||
_alarm.arm_away()
|
_alarm.arm_away()
|
||||||
self.update_ha_state()
|
|
||||||
|
|
||||||
def _validate_code(self, code, state):
|
def _validate_code(self, code, state):
|
||||||
""" Validate given code. """
|
""" Validate given code. """
|
||||||
|
105
homeassistant/components/alarm_control_panel/nx584.py
Normal file
105
homeassistant/components/alarm_control_panel/nx584.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.alarm_control_panel.nx584
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Support for NX584 alarm control panels.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/alarm_control_panel.nx584/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from homeassistant.const import (STATE_UNKNOWN, STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMED_AWAY)
|
||||||
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
|
|
||||||
|
REQUIREMENTS = ['pynx584==0.1']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Setup nx584. """
|
||||||
|
host = config.get('host', 'localhost:5007')
|
||||||
|
|
||||||
|
try:
|
||||||
|
add_devices([NX584Alarm(hass, host, config.get('name', 'NX584'))])
|
||||||
|
except requests.exceptions.ConnectionError as ex:
|
||||||
|
_LOGGER.error('Unable to connect to NX584: %s', str(ex))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class NX584Alarm(alarm.AlarmControlPanel):
|
||||||
|
""" NX584-based alarm panel. """
|
||||||
|
def __init__(self, hass, host, name):
|
||||||
|
from nx584 import client
|
||||||
|
self._hass = hass
|
||||||
|
self._host = host
|
||||||
|
self._name = name
|
||||||
|
self._alarm = client.Client('http://%s' % host)
|
||||||
|
# Do an initial list operation so that we will try to actually
|
||||||
|
# talk to the API and trigger a requests exception for setup_platform()
|
||||||
|
# to catch
|
||||||
|
self._alarm.list_zones()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
""" Polling needed. """
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Returns the name of the device. """
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code_format(self):
|
||||||
|
""" Characters if code is defined. """
|
||||||
|
return '[0-9]{4}([0-9]{2})?'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" Returns the state of the device. """
|
||||||
|
try:
|
||||||
|
part = self._alarm.list_partitions()[0]
|
||||||
|
zones = self._alarm.list_zones()
|
||||||
|
except requests.exceptions.ConnectionError as ex:
|
||||||
|
_LOGGER.error('Unable to connect to %(host)s: %(reason)s',
|
||||||
|
dict(host=self._host, reason=ex))
|
||||||
|
return STATE_UNKNOWN
|
||||||
|
except IndexError:
|
||||||
|
_LOGGER.error('nx584 reports no partitions')
|
||||||
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
|
bypassed = False
|
||||||
|
for zone in zones:
|
||||||
|
if zone['bypassed']:
|
||||||
|
_LOGGER.debug('Zone %(zone)s is bypassed, '
|
||||||
|
'assuming HOME',
|
||||||
|
dict(zone=zone['number']))
|
||||||
|
bypassed = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not part['armed']:
|
||||||
|
return STATE_ALARM_DISARMED
|
||||||
|
elif bypassed:
|
||||||
|
return STATE_ALARM_ARMED_HOME
|
||||||
|
else:
|
||||||
|
return STATE_ALARM_ARMED_AWAY
|
||||||
|
|
||||||
|
def alarm_disarm(self, code=None):
|
||||||
|
""" Send disarm command. """
|
||||||
|
self._alarm.disarm(code)
|
||||||
|
|
||||||
|
def alarm_arm_home(self, code=None):
|
||||||
|
""" Send arm home command. """
|
||||||
|
self._alarm.arm('home')
|
||||||
|
|
||||||
|
def alarm_arm_away(self, code=None):
|
||||||
|
""" Send arm away command. """
|
||||||
|
self._alarm.arm('auto')
|
||||||
|
|
||||||
|
def alarm_trigger(self, code=None):
|
||||||
|
""" Alarm trigger command. """
|
||||||
|
raise NotImplementedError()
|
84
homeassistant/components/apcupsd.py
Normal file
84
homeassistant/components/apcupsd.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.apcupsd
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Sets up and provides access to the status output of APCUPSd via its Network
|
||||||
|
Information Server (NIS).
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/apcupsd/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
DOMAIN = "apcupsd"
|
||||||
|
REQUIREMENTS = ("apcaccess==0.0.4",)
|
||||||
|
|
||||||
|
CONF_HOST = "host"
|
||||||
|
CONF_PORT = "port"
|
||||||
|
CONF_TYPE = "type"
|
||||||
|
|
||||||
|
DEFAULT_HOST = "localhost"
|
||||||
|
DEFAULT_PORT = 3551
|
||||||
|
|
||||||
|
KEY_STATUS = "STATUS"
|
||||||
|
|
||||||
|
VALUE_ONLINE = "ONLINE"
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||||
|
|
||||||
|
DATA = None
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
""" Use config values to set up a function enabling status retrieval. """
|
||||||
|
global DATA
|
||||||
|
|
||||||
|
host = config[DOMAIN].get(CONF_HOST, DEFAULT_HOST)
|
||||||
|
port = config[DOMAIN].get(CONF_PORT, DEFAULT_PORT)
|
||||||
|
|
||||||
|
DATA = APCUPSdData(host, port)
|
||||||
|
|
||||||
|
# It doesn't really matter why we're not able to get the status, just that
|
||||||
|
# we can't.
|
||||||
|
# pylint: disable=broad-except
|
||||||
|
try:
|
||||||
|
DATA.update(no_throttle=True)
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception("Failure while testing APCUPSd status retrieval.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class APCUPSdData(object):
|
||||||
|
"""
|
||||||
|
Stores the data retrieved from APCUPSd for each entity to use, acts as the
|
||||||
|
single point responsible for fetching updates from the server.
|
||||||
|
"""
|
||||||
|
def __init__(self, host, port):
|
||||||
|
from apcaccess import status
|
||||||
|
self._host = host
|
||||||
|
self._port = port
|
||||||
|
self._status = None
|
||||||
|
self._get = status.get
|
||||||
|
self._parse = status.parse
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self):
|
||||||
|
""" Get latest update if throttle allows. Return status. """
|
||||||
|
self.update()
|
||||||
|
return self._status
|
||||||
|
|
||||||
|
def _get_status(self):
|
||||||
|
""" Get the status from APCUPSd and parse it into a dict. """
|
||||||
|
return self._parse(self._get(host=self._host, port=self._port))
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
|
def update(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Fetch the latest status from APCUPSd and store it in self._status.
|
||||||
|
"""
|
||||||
|
self._status = self._get_status()
|
44
homeassistant/components/binary_sensor/apcupsd.py
Normal file
44
homeassistant/components/binary_sensor/apcupsd.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.binary_sensor.apcupsd
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Provides a binary sensor to track online status of a UPS.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/binary_sensor.apcupsd/
|
||||||
|
"""
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
|
from homeassistant.components import apcupsd
|
||||||
|
|
||||||
|
DEPENDENCIES = [apcupsd.DOMAIN]
|
||||||
|
DEFAULT_NAME = "UPS Online Status"
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
""" Instantiate an OnlineStatus binary sensor entity and add it to HA. """
|
||||||
|
add_entities((OnlineStatus(config, apcupsd.DATA),))
|
||||||
|
|
||||||
|
|
||||||
|
class OnlineStatus(BinarySensorDevice):
|
||||||
|
""" Binary sensor to represent UPS online status. """
|
||||||
|
def __init__(self, config, data):
|
||||||
|
self._config = config
|
||||||
|
self._data = data
|
||||||
|
self._state = None
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" The name of the UPS online status sensor. """
|
||||||
|
return self._config.get("name", DEFAULT_NAME)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
""" True if the UPS is online, else False. """
|
||||||
|
return self._state == apcupsd.VALUE_ONLINE
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""
|
||||||
|
Get the status report from APCUPSd (or cache) and set this entity's
|
||||||
|
state.
|
||||||
|
"""
|
||||||
|
self._state = self._data.status[apcupsd.KEY_STATUS]
|
@ -1,8 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
homeassistant.components.binary_sensor.command_sensor
|
homeassistant.components.binary_sensor.command_sensor
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Allows to configure custom shell commands to turn a value
|
Allows to configure custom shell commands to turn a value
|
||||||
into a logical value for a binary sensor.
|
into a logical value for a binary sensor.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/binary_sensor.command/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
@ -13,10 +13,11 @@ import homeassistant.components.nest as nest
|
|||||||
from homeassistant.components.sensor.nest import NestSensor
|
from homeassistant.components.sensor.nest import NestSensor
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
|
|
||||||
|
DEPENDENCIES = ['nest']
|
||||||
BINARY_TYPES = ['fan',
|
BINARY_TYPES = ['fan',
|
||||||
'hvac_ac_state',
|
'hvac_ac_state',
|
||||||
'hvac_aux_heater_state',
|
'hvac_aux_heater_state',
|
||||||
|
'hvac_heater_state',
|
||||||
'hvac_heat_x2_state',
|
'hvac_heat_x2_state',
|
||||||
'hvac_heat_x3_state',
|
'hvac_heat_x3_state',
|
||||||
'hvac_alt_heat_state',
|
'hvac_alt_heat_state',
|
||||||
|
@ -21,7 +21,7 @@ DEFAULT_METHOD = 'GET'
|
|||||||
|
|
||||||
# pylint: disable=unused-variable
|
# pylint: disable=unused-variable
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup REST binary sensors."""
|
""" Setup REST binary sensors. """
|
||||||
resource = config.get('resource', None)
|
resource = config.get('resource', None)
|
||||||
method = config.get('method', DEFAULT_METHOD)
|
method = config.get('method', DEFAULT_METHOD)
|
||||||
payload = config.get('payload', None)
|
payload = config.get('payload', None)
|
||||||
@ -41,10 +41,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
class RestBinarySensor(BinarySensorDevice):
|
class RestBinarySensor(BinarySensorDevice):
|
||||||
"""REST binary sensor."""
|
""" A REST binary sensor. """
|
||||||
|
|
||||||
def __init__(self, hass, rest, name, value_template):
|
def __init__(self, hass, rest, name, value_template):
|
||||||
"""Initialize a REST binary sensor."""
|
""" Initialize a REST binary sensor. """
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self.rest = rest
|
self.rest = rest
|
||||||
self._name = name
|
self._name = name
|
||||||
@ -54,12 +54,12 @@ class RestBinarySensor(BinarySensorDevice):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Name of the binary sensor."""
|
""" Name of the binary sensor. """
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return if the binary sensor is on."""
|
""" Return if the binary sensor is on. """
|
||||||
if self.rest.data is None:
|
if self.rest.data is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -69,5 +69,5 @@ class RestBinarySensor(BinarySensorDevice):
|
|||||||
return bool(int(self.rest.data))
|
return bool(int(self.rest.data))
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get the latest data from REST API and updates the state."""
|
""" Get the latest data from REST API and updates the state. """
|
||||||
self.rest.update()
|
self.rest.update()
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
homeassistant.components.binary_sensor.zigbee
|
homeassistant.components.binary_sensor.zigbee
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Contains functionality to use a ZigBee device as a binary sensor.
|
Contains functionality to use a ZigBee device as a binary sensor.
|
||||||
"""
|
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/binary_sensor.zigbee/
|
||||||
|
"""
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.components.zigbee import (
|
from homeassistant.components.zigbee import (
|
||||||
ZigBeeDigitalIn, ZigBeeDigitalInConfig)
|
ZigBeeDigitalIn, ZigBeeDigitalInConfig)
|
||||||
@ -13,9 +15,7 @@ DEPENDENCIES = ["zigbee"]
|
|||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""
|
""" Create and add an entity based on the configuration. """
|
||||||
Create and add an entity based on the configuration.
|
|
||||||
"""
|
|
||||||
add_entities([
|
add_entities([
|
||||||
ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))
|
ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))
|
||||||
])
|
])
|
||||||
|
77
homeassistant/components/bloomsky.py
Normal file
77
homeassistant/components/bloomsky.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.bloomsky
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Support for BloomSky weather station.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/bloomsky/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
import requests
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
from homeassistant.helpers import validate_config
|
||||||
|
from homeassistant.const import CONF_API_KEY
|
||||||
|
|
||||||
|
DOMAIN = "bloomsky"
|
||||||
|
BLOOMSKY = None
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# The BloomSky only updates every 5-8 minutes as per the API spec so there's
|
||||||
|
# no point in polling the API more frequently
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument,too-few-public-methods
|
||||||
|
def setup(hass, config):
|
||||||
|
""" Setup BloomSky component. """
|
||||||
|
if not validate_config(
|
||||||
|
config,
|
||||||
|
{DOMAIN: [CONF_API_KEY]},
|
||||||
|
_LOGGER):
|
||||||
|
return False
|
||||||
|
|
||||||
|
api_key = config[DOMAIN][CONF_API_KEY]
|
||||||
|
|
||||||
|
global BLOOMSKY
|
||||||
|
try:
|
||||||
|
BLOOMSKY = BloomSky(api_key)
|
||||||
|
except RuntimeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class BloomSky(object):
|
||||||
|
""" Handle all communication with the BloomSky API. """
|
||||||
|
|
||||||
|
# API documentation at http://weatherlution.com/bloomsky-api/
|
||||||
|
|
||||||
|
API_URL = "https://api.bloomsky.com/api/skydata"
|
||||||
|
|
||||||
|
def __init__(self, api_key):
|
||||||
|
self._api_key = api_key
|
||||||
|
self.devices = {}
|
||||||
|
_LOGGER.debug("Initial bloomsky device load...")
|
||||||
|
self.refresh_devices()
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
|
def refresh_devices(self):
|
||||||
|
"""
|
||||||
|
Uses the API to retreive a list of devices associated with an
|
||||||
|
account along with all the sensors on the device.
|
||||||
|
"""
|
||||||
|
_LOGGER.debug("Fetching bloomsky update")
|
||||||
|
response = requests.get(self.API_URL,
|
||||||
|
headers={"Authorization": self._api_key},
|
||||||
|
timeout=10)
|
||||||
|
if response.status_code == 401:
|
||||||
|
raise RuntimeError("Invalid API_KEY")
|
||||||
|
elif response.status_code != 200:
|
||||||
|
_LOGGER.error("Invalid HTTP response: %s", response.status_code)
|
||||||
|
return
|
||||||
|
# create dictionary keyed off of the device unique id
|
||||||
|
self.devices.update({
|
||||||
|
device["DeviceID"]: device for device in response.json()
|
||||||
|
})
|
@ -33,8 +33,6 @@ SWITCH_ACTION_SNAPSHOT = 'snapshot'
|
|||||||
|
|
||||||
SERVICE_CAMERA = 'camera_service'
|
SERVICE_CAMERA = 'camera_service'
|
||||||
|
|
||||||
STATE_RECORDING = 'recording'
|
|
||||||
|
|
||||||
DEFAULT_RECORDING_SECONDS = 30
|
DEFAULT_RECORDING_SECONDS = 30
|
||||||
|
|
||||||
# Maps discovered services to their platforms
|
# Maps discovered services to their platforms
|
||||||
@ -46,6 +44,7 @@ DIR_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S'
|
|||||||
REC_DIR_PREFIX = 'recording-'
|
REC_DIR_PREFIX = 'recording-'
|
||||||
REC_IMG_PREFIX = 'recording_image-'
|
REC_IMG_PREFIX = 'recording_image-'
|
||||||
|
|
||||||
|
STATE_RECORDING = 'recording'
|
||||||
STATE_STREAMING = 'streaming'
|
STATE_STREAMING = 'streaming'
|
||||||
STATE_IDLE = 'idle'
|
STATE_IDLE = 'idle'
|
||||||
|
|
||||||
@ -121,33 +120,7 @@ def setup(hass, config):
|
|||||||
try:
|
try:
|
||||||
camera.is_streaming = True
|
camera.is_streaming = True
|
||||||
camera.update_ha_state()
|
camera.update_ha_state()
|
||||||
|
camera.mjpeg_stream(handler)
|
||||||
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
|
|
||||||
handler.request.sendall(bytes(
|
|
||||||
'Content-type: multipart/x-mixed-replace; \
|
|
||||||
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
|
|
||||||
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
|
|
||||||
|
|
||||||
# MJPEG_START_HEADER.format()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
img_bytes = camera.camera_image()
|
|
||||||
if img_bytes is None:
|
|
||||||
continue
|
|
||||||
headers_str = '\r\n'.join((
|
|
||||||
'Content-length: {}'.format(len(img_bytes)),
|
|
||||||
'Content-type: image/jpeg',
|
|
||||||
)) + '\r\n\r\n'
|
|
||||||
|
|
||||||
handler.request.sendall(
|
|
||||||
bytes(headers_str, 'utf-8') +
|
|
||||||
img_bytes +
|
|
||||||
bytes('\r\n', 'utf-8'))
|
|
||||||
|
|
||||||
handler.request.sendall(
|
|
||||||
bytes('--jpgboundary\r\n', 'utf-8'))
|
|
||||||
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
except (requests.RequestException, IOError):
|
except (requests.RequestException, IOError):
|
||||||
camera.is_streaming = False
|
camera.is_streaming = False
|
||||||
@ -190,6 +163,34 @@ class Camera(Entity):
|
|||||||
""" Return bytes of camera image. """
|
""" Return bytes of camera image. """
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def mjpeg_stream(self, handler):
|
||||||
|
""" Generate an HTTP MJPEG stream from camera images. """
|
||||||
|
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
|
||||||
|
handler.request.sendall(bytes(
|
||||||
|
'Content-type: multipart/x-mixed-replace; \
|
||||||
|
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
|
||||||
|
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
|
||||||
|
|
||||||
|
# MJPEG_START_HEADER.format()
|
||||||
|
while True:
|
||||||
|
img_bytes = self.camera_image()
|
||||||
|
if img_bytes is None:
|
||||||
|
continue
|
||||||
|
headers_str = '\r\n'.join((
|
||||||
|
'Content-length: {}'.format(len(img_bytes)),
|
||||||
|
'Content-type: image/jpeg',
|
||||||
|
)) + '\r\n\r\n'
|
||||||
|
|
||||||
|
handler.request.sendall(
|
||||||
|
bytes(headers_str, 'utf-8') +
|
||||||
|
img_bytes +
|
||||||
|
bytes('\r\n', 'utf-8'))
|
||||||
|
|
||||||
|
handler.request.sendall(
|
||||||
|
bytes('--jpgboundary\r\n', 'utf-8'))
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
""" Returns the state of the entity. """
|
""" Returns the state of the entity. """
|
||||||
|
60
homeassistant/components/camera/bloomsky.py
Normal file
60
homeassistant/components/camera/bloomsky.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.camera.bloomsky
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Support for a camera of a BloomSky weather station.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/camera.bloomsky/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import homeassistant.components.bloomsky as bloomsky
|
||||||
|
from homeassistant.components.camera import Camera
|
||||||
|
|
||||||
|
DEPENDENCIES = ["bloomsky"]
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
|
""" set up access to BloomSky cameras """
|
||||||
|
for device in bloomsky.BLOOMSKY.devices.values():
|
||||||
|
add_devices_callback([BloomSkyCamera(bloomsky.BLOOMSKY, device)])
|
||||||
|
|
||||||
|
|
||||||
|
class BloomSkyCamera(Camera):
|
||||||
|
""" Represents the images published from the BloomSky's camera. """
|
||||||
|
|
||||||
|
def __init__(self, bs, device):
|
||||||
|
""" set up for access to the BloomSky camera images """
|
||||||
|
super(BloomSkyCamera, self).__init__()
|
||||||
|
self._name = device["DeviceName"]
|
||||||
|
self._id = device["DeviceID"]
|
||||||
|
self._bloomsky = bs
|
||||||
|
self._url = ""
|
||||||
|
self._last_url = ""
|
||||||
|
# _last_image will store images as they are downloaded so that the
|
||||||
|
# frequent updates in home-assistant don't keep poking the server
|
||||||
|
# to download the same image over and over
|
||||||
|
self._last_image = ""
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def camera_image(self):
|
||||||
|
""" Update the camera's image if it has changed. """
|
||||||
|
try:
|
||||||
|
self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"]
|
||||||
|
self._bloomsky.refresh_devices()
|
||||||
|
# if the url hasn't changed then the image hasn't changed
|
||||||
|
if self._url != self._last_url:
|
||||||
|
response = requests.get(self._url, timeout=10)
|
||||||
|
self._last_url = self._url
|
||||||
|
self._last_image = response.content
|
||||||
|
except requests.exceptions.RequestException as error:
|
||||||
|
self._logger.error("Error getting bloomsky image: %s", error)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._last_image
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" The name of this BloomSky device. """
|
||||||
|
return self._name
|
@ -14,6 +14,9 @@ from requests.auth import HTTPBasicAuth
|
|||||||
|
|
||||||
from homeassistant.helpers import validate_config
|
from homeassistant.helpers import validate_config
|
||||||
from homeassistant.components.camera import DOMAIN, Camera
|
from homeassistant.components.camera import DOMAIN, Camera
|
||||||
|
from homeassistant.const import HTTP_OK
|
||||||
|
|
||||||
|
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -41,6 +44,17 @@ class MjpegCamera(Camera):
|
|||||||
self._password = device_info.get('password')
|
self._password = device_info.get('password')
|
||||||
self._mjpeg_url = device_info['mjpeg_url']
|
self._mjpeg_url = device_info['mjpeg_url']
|
||||||
|
|
||||||
|
def camera_stream(self):
|
||||||
|
""" Return a mjpeg stream image response directly from the camera. """
|
||||||
|
if self._username and self._password:
|
||||||
|
return requests.get(self._mjpeg_url,
|
||||||
|
auth=HTTPBasicAuth(self._username,
|
||||||
|
self._password),
|
||||||
|
stream=True)
|
||||||
|
else:
|
||||||
|
return requests.get(self._mjpeg_url,
|
||||||
|
stream=True)
|
||||||
|
|
||||||
def camera_image(self):
|
def camera_image(self):
|
||||||
""" Return a still image response from the camera. """
|
""" Return a still image response from the camera. """
|
||||||
|
|
||||||
@ -55,16 +69,22 @@ class MjpegCamera(Camera):
|
|||||||
jpg = data[jpg_start:jpg_end + 2]
|
jpg = data[jpg_start:jpg_end + 2]
|
||||||
return jpg
|
return jpg
|
||||||
|
|
||||||
if self._username and self._password:
|
with closing(self.camera_stream()) as response:
|
||||||
with closing(requests.get(self._mjpeg_url,
|
return process_response(response)
|
||||||
auth=HTTPBasicAuth(self._username,
|
|
||||||
self._password),
|
def mjpeg_stream(self, handler):
|
||||||
stream=True)) as response:
|
""" Generate an HTTP MJPEG stream from the camera. """
|
||||||
return process_response(response)
|
response = self.camera_stream()
|
||||||
else:
|
content_type = response.headers[CONTENT_TYPE_HEADER]
|
||||||
with closing(requests.get(self._mjpeg_url,
|
|
||||||
stream=True)) as response:
|
handler.send_response(HTTP_OK)
|
||||||
return process_response(response)
|
handler.send_header(CONTENT_TYPE_HEADER, content_type)
|
||||||
|
handler.end_headers()
|
||||||
|
|
||||||
|
for chunk in response.iter_content(chunk_size=1024):
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
handler.wfile.write(chunk)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
91
homeassistant/components/camera/uvc.py
Normal file
91
homeassistant/components/camera/uvc.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.camera.uvc
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Support for Ubiquiti's UVC cameras.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/camera.uvc/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from homeassistant.helpers import validate_config
|
||||||
|
from homeassistant.components.camera import DOMAIN, Camera
|
||||||
|
|
||||||
|
REQUIREMENTS = ['uvcclient==0.5']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Discover cameras on a Unifi NVR. """
|
||||||
|
if not validate_config({DOMAIN: config}, {DOMAIN: ['nvr', 'key']},
|
||||||
|
_LOGGER):
|
||||||
|
return None
|
||||||
|
|
||||||
|
addr = config.get('nvr')
|
||||||
|
port = int(config.get('port', 7080))
|
||||||
|
key = config.get('key')
|
||||||
|
|
||||||
|
from uvcclient import nvr
|
||||||
|
nvrconn = nvr.UVCRemote(addr, port, key)
|
||||||
|
try:
|
||||||
|
cameras = nvrconn.index()
|
||||||
|
except nvr.NotAuthorized:
|
||||||
|
_LOGGER.error('Authorization failure while connecting to NVR')
|
||||||
|
return False
|
||||||
|
except nvr.NvrError:
|
||||||
|
_LOGGER.error('NVR refuses to talk to me')
|
||||||
|
return False
|
||||||
|
except requests.exceptions.ConnectionError as ex:
|
||||||
|
_LOGGER.error('Unable to connect to NVR: %s', str(ex))
|
||||||
|
return False
|
||||||
|
|
||||||
|
for camera in cameras:
|
||||||
|
add_devices([UnifiVideoCamera(nvrconn,
|
||||||
|
camera['uuid'],
|
||||||
|
camera['name'])])
|
||||||
|
|
||||||
|
|
||||||
|
class UnifiVideoCamera(Camera):
|
||||||
|
""" A Ubiquiti Unifi Video Camera. """
|
||||||
|
|
||||||
|
def __init__(self, nvr, uuid, name):
|
||||||
|
super(UnifiVideoCamera, self).__init__()
|
||||||
|
self._nvr = nvr
|
||||||
|
self._uuid = uuid
|
||||||
|
self._name = name
|
||||||
|
self.is_streaming = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_recording(self):
|
||||||
|
caminfo = self._nvr.get_camera(self._uuid)
|
||||||
|
return caminfo['recordingSettings']['fullTimeRecordEnabled']
|
||||||
|
|
||||||
|
def camera_image(self):
|
||||||
|
from uvcclient import camera as uvc_camera
|
||||||
|
|
||||||
|
caminfo = self._nvr.get_camera(self._uuid)
|
||||||
|
camera = None
|
||||||
|
for addr in [caminfo['host'], caminfo['internalHost']]:
|
||||||
|
try:
|
||||||
|
camera = uvc_camera.UVCCameraClient(addr,
|
||||||
|
caminfo['username'],
|
||||||
|
'ubnt')
|
||||||
|
_LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s',
|
||||||
|
dict(name=self._name, addr=addr))
|
||||||
|
except socket.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not camera:
|
||||||
|
_LOGGER.error('Unable to login to camera')
|
||||||
|
return None
|
||||||
|
|
||||||
|
camera.login()
|
||||||
|
return camera.get_snapshot()
|
@ -141,7 +141,7 @@ class Configurator(object):
|
|||||||
|
|
||||||
state = self.hass.states.get(entity_id)
|
state = self.hass.states.get(entity_id)
|
||||||
|
|
||||||
new_data = state.attributes
|
new_data = dict(state.attributes)
|
||||||
new_data[ATTR_ERRORS] = error
|
new_data[ATTR_ERRORS] = error
|
||||||
|
|
||||||
self.hass.states.set(entity_id, STATE_CONFIGURE, new_data)
|
self.hass.states.set(entity_id, STATE_CONFIGURE, new_data)
|
||||||
|
@ -21,6 +21,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
|||||||
'binary_sensor',
|
'binary_sensor',
|
||||||
'camera',
|
'camera',
|
||||||
'device_tracker',
|
'device_tracker',
|
||||||
|
'garage_door',
|
||||||
'light',
|
'light',
|
||||||
'lock',
|
'lock',
|
||||||
'media_player',
|
'media_player',
|
||||||
|
@ -11,7 +11,6 @@ import logging
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import telnetlib
|
|
||||||
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||||
from homeassistant.helpers import validate_config
|
from homeassistant.helpers import validate_config
|
||||||
@ -21,6 +20,7 @@ from homeassistant.components.device_tracker import DOMAIN
|
|||||||
# Return cached results if last scan was less then this time ago
|
# Return cached results if last scan was less then this time ago
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||||
|
|
||||||
|
REQUIREMENTS = ['pexpect==4.0.1']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
_DEVICES_REGEX = re.compile(
|
_DEVICES_REGEX = re.compile(
|
||||||
@ -44,6 +44,7 @@ def get_scanner(hass, config):
|
|||||||
|
|
||||||
class ArubaDeviceScanner(object):
|
class ArubaDeviceScanner(object):
|
||||||
""" This class queries a Aruba Acces Point for connected devices. """
|
""" This class queries a Aruba Acces Point for connected devices. """
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.host = config[CONF_HOST]
|
self.host = config[CONF_HOST]
|
||||||
self.username = config[CONF_USERNAME]
|
self.username = config[CONF_USERNAME]
|
||||||
@ -93,23 +94,39 @@ class ArubaDeviceScanner(object):
|
|||||||
|
|
||||||
def get_aruba_data(self):
|
def get_aruba_data(self):
|
||||||
""" Retrieve data from Aruba Access Point and return parsed result. """
|
""" Retrieve data from Aruba Access Point and return parsed result. """
|
||||||
try:
|
|
||||||
telnet = telnetlib.Telnet(self.host)
|
import pexpect
|
||||||
telnet.read_until(b'User: ')
|
connect = "ssh {}@{}"
|
||||||
telnet.write((self.username + '\r\n').encode('ascii'))
|
ssh = pexpect.spawn(connect.format(self.username, self.host))
|
||||||
telnet.read_until(b'Password: ')
|
query = ssh.expect(['password:', pexpect.TIMEOUT, pexpect.EOF,
|
||||||
telnet.write((self.password + '\r\n').encode('ascii'))
|
'continue connecting (yes/no)?',
|
||||||
telnet.read_until(b'#')
|
'Host key verification failed.',
|
||||||
telnet.write(('show clients\r\n').encode('ascii'))
|
'Connection refused',
|
||||||
devices_result = telnet.read_until(b'#').split(b'\r\n')
|
'Connection timed out'], timeout=120)
|
||||||
telnet.write('exit\r\n'.encode('ascii'))
|
if query == 1:
|
||||||
except EOFError:
|
_LOGGER.error("Timeout")
|
||||||
_LOGGER.exception("Unexpected response from router")
|
|
||||||
return
|
return
|
||||||
except ConnectionRefusedError:
|
elif query == 2:
|
||||||
_LOGGER.exception("Connection refused by router," +
|
_LOGGER.error("Unexpected response from router")
|
||||||
" is telnet enabled?")
|
|
||||||
return
|
return
|
||||||
|
elif query == 3:
|
||||||
|
ssh.sendline('yes')
|
||||||
|
ssh.expect('password:')
|
||||||
|
elif query == 4:
|
||||||
|
_LOGGER.error("Host key Changed")
|
||||||
|
return
|
||||||
|
elif query == 5:
|
||||||
|
_LOGGER.error("Connection refused by server")
|
||||||
|
return
|
||||||
|
elif query == 6:
|
||||||
|
_LOGGER.error("Connection timed out")
|
||||||
|
return
|
||||||
|
ssh.sendline(self.password)
|
||||||
|
ssh.expect('#')
|
||||||
|
ssh.sendline('show clients')
|
||||||
|
ssh.expect('#')
|
||||||
|
devices_result = ssh.before.split(b'\r\n')
|
||||||
|
ssh.sendline('exit')
|
||||||
|
|
||||||
devices = {}
|
devices = {}
|
||||||
for device in devices_result:
|
for device in devices_result:
|
||||||
@ -119,5 +136,5 @@ class ArubaDeviceScanner(object):
|
|||||||
'ip': match.group('ip'),
|
'ip': match.group('ip'),
|
||||||
'mac': match.group('mac').upper(),
|
'mac': match.group('mac').upper(),
|
||||||
'name': match.group('name')
|
'name': match.group('name')
|
||||||
}
|
}
|
||||||
return devices
|
return devices
|
||||||
|
@ -95,7 +95,8 @@ def setup_scanner(hass, config, see):
|
|||||||
MOBILE_BEACONS_ACTIVE[dev_id].append(location)
|
MOBILE_BEACONS_ACTIVE[dev_id].append(location)
|
||||||
else:
|
else:
|
||||||
# Normal region
|
# Normal region
|
||||||
kwargs['location_name'] = location
|
if not zone.attributes.get('passive'):
|
||||||
|
kwargs['location_name'] = location
|
||||||
|
|
||||||
regions = REGIONS_ENTERED[dev_id]
|
regions = REGIONS_ENTERED[dev_id]
|
||||||
if location not in regions:
|
if location not in regions:
|
||||||
@ -115,7 +116,8 @@ def setup_scanner(hass, config, see):
|
|||||||
if new_region:
|
if new_region:
|
||||||
# Exit to previous region
|
# Exit to previous region
|
||||||
zone = hass.states.get("zone.{}".format(new_region))
|
zone = hass.states.get("zone.{}".format(new_region))
|
||||||
kwargs['location_name'] = new_region
|
if not zone.attributes.get('passive'):
|
||||||
|
kwargs['location_name'] = new_region
|
||||||
_set_gps_from_zone(kwargs, zone)
|
_set_gps_from_zone(kwargs, zone)
|
||||||
_LOGGER.info("Exit from to %s", new_region)
|
_LOGGER.info("Exit from to %s", new_region)
|
||||||
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
""" DO NOT MODIFY. Auto-generated by update_mdi script """
|
""" DO NOT MODIFY. Auto-generated by update_mdi script """
|
||||||
VERSION = "a2605736c8d959d50c4bcbba1e6a6aa5"
|
VERSION = "a1a203680639ff1abcc7b68cdb29c57a"
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||||
VERSION = "1e89871aaae43c91b2508f52bc161b69"
|
VERSION = "833d09737fec24f9219efae87c5bfd2a"
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
Subproject commit 472f485a7e17d4ec4fc7cc6c17bcd6c41830d6fa
|
Subproject commit 1380e59e182c7d5468c55c67c8c363ff9248349a
|
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
|||||||
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={exports:{},id:r,loaded:!1};return e[r].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([/*!*************************************!*\
|
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={exports:{},id:r,loaded:!1};return e[r].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([/*!*************************************!*\
|
||||||
!*** ./src/service-worker/index.js ***!
|
!*** ./src/service-worker/index.js ***!
|
||||||
\*************************************/
|
\*************************************/
|
||||||
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){var r=fetch(e.request).then(function(e){return t.put(s,e.clone()),e});return n||r})}))})}]);
|
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}]);
|
||||||
//# sourceMappingURL=service_worker.js.map
|
//# sourceMappingURL=service_worker.js.map
|
108
homeassistant/components/garage_door/__init__.py
Normal file
108
homeassistant/components/garage_door/__init__.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.garage_door
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Component to interface with garage doors that can be controlled remotely.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation
|
||||||
|
at https://home-assistant.io/components/garage_door/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from homeassistant.config import load_yaml_config_file
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN,
|
||||||
|
ATTR_ENTITY_ID)
|
||||||
|
from homeassistant.components import (group, wink)
|
||||||
|
|
||||||
|
DOMAIN = 'garage_door'
|
||||||
|
SCAN_INTERVAL = 30
|
||||||
|
|
||||||
|
GROUP_NAME_ALL_GARAGE_DOORS = 'all garage doors'
|
||||||
|
ENTITY_ID_ALL_GARAGE_DOORS = group.ENTITY_ID_FORMAT.format('all_garage_doors')
|
||||||
|
|
||||||
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
|
|
||||||
|
# Maps discovered services to their platforms
|
||||||
|
DISCOVERY_PLATFORMS = {
|
||||||
|
wink.DISCOVER_GARAGE_DOORS: 'wink'
|
||||||
|
}
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def is_closed(hass, entity_id=None):
|
||||||
|
""" Returns if the garage door is closed based on the statemachine. """
|
||||||
|
entity_id = entity_id or ENTITY_ID_ALL_GARAGE_DOORS
|
||||||
|
return hass.states.is_state(entity_id, STATE_CLOSED)
|
||||||
|
|
||||||
|
|
||||||
|
def close_door(hass, entity_id=None):
|
||||||
|
""" Closes all or specified garage door. """
|
||||||
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||||
|
hass.services.call(DOMAIN, SERVICE_CLOSE, data)
|
||||||
|
|
||||||
|
|
||||||
|
def open_door(hass, entity_id=None):
|
||||||
|
""" Open all or specified garage door. """
|
||||||
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||||
|
hass.services.call(DOMAIN, SERVICE_OPEN, data)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
""" Track states and offer events for garage door. """
|
||||||
|
component = EntityComponent(
|
||||||
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
|
||||||
|
GROUP_NAME_ALL_GARAGE_DOORS)
|
||||||
|
component.setup(config)
|
||||||
|
|
||||||
|
def handle_garage_door_service(service):
|
||||||
|
""" Handles calls to the garage door services. """
|
||||||
|
target_locks = component.extract_from_service(service)
|
||||||
|
|
||||||
|
for item in target_locks:
|
||||||
|
if service.service == SERVICE_CLOSE:
|
||||||
|
item.close_door()
|
||||||
|
else:
|
||||||
|
item.open_door()
|
||||||
|
|
||||||
|
if item.should_poll:
|
||||||
|
item.update_ha_state(True)
|
||||||
|
|
||||||
|
descriptions = load_yaml_config_file(
|
||||||
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
hass.services.register(DOMAIN, SERVICE_OPEN, handle_garage_door_service,
|
||||||
|
descriptions.get(SERVICE_OPEN))
|
||||||
|
hass.services.register(DOMAIN, SERVICE_CLOSE, handle_garage_door_service,
|
||||||
|
descriptions.get(SERVICE_CLOSE))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class GarageDoorDevice(Entity):
|
||||||
|
""" Represents a garage door. """
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
""" Is the garage door closed or opened. """
|
||||||
|
return None
|
||||||
|
|
||||||
|
def close_door(self):
|
||||||
|
""" Closes the garage door. """
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def open_door(self):
|
||||||
|
""" Opens the garage door. """
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" State of the garage door. """
|
||||||
|
closed = self.is_closed
|
||||||
|
if closed is None:
|
||||||
|
return STATE_UNKNOWN
|
||||||
|
return STATE_CLOSED if closed else STATE_OPEN
|
48
homeassistant/components/garage_door/demo.py
Normal file
48
homeassistant/components/garage_door/demo.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.garage_door.demo
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Demo platform that has two fake garage doors.
|
||||||
|
"""
|
||||||
|
from homeassistant.components.garage_door import GarageDoorDevice
|
||||||
|
from homeassistant.const import STATE_CLOSED, STATE_OPEN
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
|
""" Find and return demo garage doors. """
|
||||||
|
add_devices_callback([
|
||||||
|
DemoGarageDoor('Left Garage Door', STATE_CLOSED),
|
||||||
|
DemoGarageDoor('Right Garage Door', STATE_OPEN)
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class DemoGarageDoor(GarageDoorDevice):
|
||||||
|
""" Provides a demo garage door. """
|
||||||
|
def __init__(self, name, state):
|
||||||
|
self._name = name
|
||||||
|
self._state = state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
""" No polling needed for a demo garage door. """
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Returns the name of the device if any. """
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
""" True if device is closed. """
|
||||||
|
return self._state == STATE_CLOSED
|
||||||
|
|
||||||
|
def close_door(self, **kwargs):
|
||||||
|
""" Close the device. """
|
||||||
|
self._state = STATE_CLOSED
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
|
def open_door(self, **kwargs):
|
||||||
|
""" Open the device. """
|
||||||
|
self._state = STATE_OPEN
|
||||||
|
self.update_ha_state()
|
0
homeassistant/components/garage_door/services.yaml
Normal file
0
homeassistant/components/garage_door/services.yaml
Normal file
67
homeassistant/components/garage_door/wink.py
Normal file
67
homeassistant/components/garage_door/wink.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.garage_door.wink
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Support for Wink garage doors.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/garage_door.wink/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.garage_door import GarageDoorDevice
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
|
||||||
|
REQUIREMENTS = ['python-wink==0.5.0']
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Sets up the Wink platform. """
|
||||||
|
import pywink
|
||||||
|
|
||||||
|
if discovery_info is None:
|
||||||
|
token = config.get(CONF_ACCESS_TOKEN)
|
||||||
|
|
||||||
|
if token is None:
|
||||||
|
logging.getLogger(__name__).error(
|
||||||
|
"Missing wink access_token. "
|
||||||
|
"Get one at https://winkbearertoken.appspot.com/")
|
||||||
|
return
|
||||||
|
|
||||||
|
pywink.set_bearer_token(token)
|
||||||
|
|
||||||
|
add_devices(WinkGarageDoorDevice(door) for door in
|
||||||
|
pywink.get_garage_doors())
|
||||||
|
|
||||||
|
|
||||||
|
class WinkGarageDoorDevice(GarageDoorDevice):
|
||||||
|
""" Represents a Wink garage door. """
|
||||||
|
|
||||||
|
def __init__(self, wink):
|
||||||
|
self.wink = wink
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
""" Returns the id of this wink garage door """
|
||||||
|
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Returns the name of the garage door if any. """
|
||||||
|
return self.wink.name()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
""" Update the state of the garage door. """
|
||||||
|
self.wink.update_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
""" True if device is closed. """
|
||||||
|
return self.wink.state() == 0
|
||||||
|
|
||||||
|
def close_door(self):
|
||||||
|
""" Close the device. """
|
||||||
|
self.wink.set_state(0)
|
||||||
|
|
||||||
|
def open_door(self):
|
||||||
|
""" Open the device. """
|
||||||
|
self.wink.set_state(1)
|
122
homeassistant/components/graphite.py
Normal file
122
homeassistant/components/graphite.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.graphite
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Component that records all events and state changes and feeds the data to
|
||||||
|
a graphite installation.
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
|
||||||
|
graphite:
|
||||||
|
host: foobar
|
||||||
|
port: 2003
|
||||||
|
prefix: ha
|
||||||
|
|
||||||
|
All config elements are optional, and assumed to be on localhost at the
|
||||||
|
default port if not specified. Prefix is the metric prefix in graphite,
|
||||||
|
and defaults to 'ha'.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import queue
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
EVENT_STATE_CHANGED,
|
||||||
|
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||||
|
from homeassistant.helpers import state
|
||||||
|
|
||||||
|
DOMAIN = "graphite"
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
""" Setup graphite feeder. """
|
||||||
|
graphite_config = config.get('graphite', {})
|
||||||
|
host = graphite_config.get('host', 'localhost')
|
||||||
|
prefix = graphite_config.get('prefix', 'ha')
|
||||||
|
try:
|
||||||
|
port = int(graphite_config.get('port', 2003))
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.error('Invalid port specified')
|
||||||
|
return False
|
||||||
|
|
||||||
|
GraphiteFeeder(hass, host, port, prefix)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class GraphiteFeeder(threading.Thread):
|
||||||
|
""" Feeds data to graphite. """
|
||||||
|
def __init__(self, hass, host, port, prefix):
|
||||||
|
super(GraphiteFeeder, self).__init__(daemon=True)
|
||||||
|
self._hass = hass
|
||||||
|
self._host = host
|
||||||
|
self._port = port
|
||||||
|
# rstrip any trailing dots in case they think they
|
||||||
|
# need it
|
||||||
|
self._prefix = prefix.rstrip('.')
|
||||||
|
self._queue = queue.Queue()
|
||||||
|
self._quit_object = object()
|
||||||
|
|
||||||
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
|
||||||
|
self.start_listen)
|
||||||
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||||
|
self.shutdown)
|
||||||
|
hass.bus.listen(EVENT_STATE_CHANGED, self.event_listener)
|
||||||
|
|
||||||
|
def start_listen(self, event):
|
||||||
|
""" Start event-processing thread. """
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def shutdown(self, event):
|
||||||
|
""" Tell the thread that we are done.
|
||||||
|
|
||||||
|
This does not block because there is nothing to
|
||||||
|
clean up (and no penalty for killing in-process
|
||||||
|
connections to graphite.
|
||||||
|
"""
|
||||||
|
self._queue.put(self._quit_object)
|
||||||
|
|
||||||
|
def event_listener(self, event):
|
||||||
|
""" Queue an event for processing. """
|
||||||
|
self._queue.put(event)
|
||||||
|
|
||||||
|
def _send_to_graphite(self, data):
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(10)
|
||||||
|
sock.connect((self._host, self._port))
|
||||||
|
sock.sendall(data.encode('ascii'))
|
||||||
|
sock.send('\n'.encode('ascii'))
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
def _report_attributes(self, entity_id, new_state):
|
||||||
|
now = time.time()
|
||||||
|
things = dict(new_state.attributes)
|
||||||
|
try:
|
||||||
|
things['state'] = state.state_as_number(new_state)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
lines = ['%s.%s.%s %f %i' % (self._prefix,
|
||||||
|
entity_id, key.replace(' ', '_'),
|
||||||
|
value, now)
|
||||||
|
for key, value in things.items()
|
||||||
|
if isinstance(value, (float, int))]
|
||||||
|
if not lines:
|
||||||
|
return
|
||||||
|
_LOGGER.debug('Sending to graphite: %s', lines)
|
||||||
|
try:
|
||||||
|
self._send_to_graphite('\n'.join(lines))
|
||||||
|
except socket.error:
|
||||||
|
_LOGGER.exception('Failed to send data to graphite')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while True:
|
||||||
|
event = self._queue.get()
|
||||||
|
if event == self._quit_object:
|
||||||
|
self._queue.task_done()
|
||||||
|
return
|
||||||
|
elif (event.event_type == EVENT_STATE_CHANGED and
|
||||||
|
'new_state' in event.data):
|
||||||
|
self._report_attributes(event.data['entity_id'],
|
||||||
|
event.data['new_state'])
|
||||||
|
self._queue.task_done()
|
@ -71,7 +71,7 @@ def expand_entity_ids(hass, entity_ids):
|
|||||||
if domain == DOMAIN:
|
if domain == DOMAIN:
|
||||||
found_ids.extend(
|
found_ids.extend(
|
||||||
ent_id for ent_id
|
ent_id for ent_id
|
||||||
in get_entity_ids(hass, entity_id)
|
in expand_entity_ids(hass, get_entity_ids(hass, entity_id))
|
||||||
if ent_id not in found_ids)
|
if ent_id not in found_ids)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -18,6 +18,8 @@ from homeassistant.const import HTTP_BAD_REQUEST
|
|||||||
DOMAIN = 'history'
|
DOMAIN = 'history'
|
||||||
DEPENDENCIES = ['recorder', 'http']
|
DEPENDENCIES = ['recorder', 'http']
|
||||||
|
|
||||||
|
SIGNIFICANT_DOMAINS = ('thermostat',)
|
||||||
|
|
||||||
URL_HISTORY_PERIOD = re.compile(
|
URL_HISTORY_PERIOD = re.compile(
|
||||||
r'/api/history/period(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
|
r'/api/history/period(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
|
||||||
|
|
||||||
@ -35,6 +37,37 @@ def last_5_states(entity_id):
|
|||||||
return recorder.query_states(query, (entity_id, ))
|
return recorder.query_states(query, (entity_id, ))
|
||||||
|
|
||||||
|
|
||||||
|
def get_significant_states(start_time, end_time=None, entity_id=None):
|
||||||
|
"""Return states changes during UTC period start_time - end_time.
|
||||||
|
|
||||||
|
Significant states are all states where there is a state change,
|
||||||
|
as well as all states from certain domains (for instance
|
||||||
|
thermostat so that we get current temperature in our graphs).
|
||||||
|
|
||||||
|
"""
|
||||||
|
where = """
|
||||||
|
(domain in ({}) or last_changed=last_updated)
|
||||||
|
AND last_updated > ?
|
||||||
|
""".format(",".join(["'%s'" % x for x in SIGNIFICANT_DOMAINS]))
|
||||||
|
|
||||||
|
data = [start_time]
|
||||||
|
|
||||||
|
if end_time is not None:
|
||||||
|
where += "AND last_updated < ? "
|
||||||
|
data.append(end_time)
|
||||||
|
|
||||||
|
if entity_id is not None:
|
||||||
|
where += "AND entity_id = ? "
|
||||||
|
data.append(entity_id.lower())
|
||||||
|
|
||||||
|
query = ("SELECT * FROM states WHERE {} "
|
||||||
|
"ORDER BY entity_id, last_updated ASC").format(where)
|
||||||
|
|
||||||
|
states = recorder.query_states(query, data)
|
||||||
|
|
||||||
|
return states_to_json(states, start_time, entity_id)
|
||||||
|
|
||||||
|
|
||||||
def state_changes_during_period(start_time, end_time=None, entity_id=None):
|
def state_changes_during_period(start_time, end_time=None, entity_id=None):
|
||||||
"""
|
"""
|
||||||
Return states changes during UTC period start_time - end_time.
|
Return states changes during UTC period start_time - end_time.
|
||||||
@ -55,20 +88,7 @@ def state_changes_during_period(start_time, end_time=None, entity_id=None):
|
|||||||
|
|
||||||
states = recorder.query_states(query, data)
|
states = recorder.query_states(query, data)
|
||||||
|
|
||||||
result = defaultdict(list)
|
return states_to_json(states, start_time, entity_id)
|
||||||
|
|
||||||
entity_ids = [entity_id] if entity_id is not None else None
|
|
||||||
|
|
||||||
# Get the states at the start time
|
|
||||||
for state in get_states(start_time, entity_ids):
|
|
||||||
state.last_changed = start_time
|
|
||||||
result[state.entity_id].append(state)
|
|
||||||
|
|
||||||
# Append all changes to it
|
|
||||||
for entity_id, group in groupby(states, lambda state: state.entity_id):
|
|
||||||
result[entity_id].extend(group)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def get_states(utc_point_in_time, entity_ids=None, run=None):
|
def get_states(utc_point_in_time, entity_ids=None, run=None):
|
||||||
@ -100,6 +120,33 @@ def get_states(utc_point_in_time, entity_ids=None, run=None):
|
|||||||
return recorder.query_states(query, where_data)
|
return recorder.query_states(query, where_data)
|
||||||
|
|
||||||
|
|
||||||
|
def states_to_json(states, start_time, entity_id):
|
||||||
|
"""Converts SQL results into JSON friendly data structure.
|
||||||
|
|
||||||
|
This takes our state list and turns it into a JSON friendly data
|
||||||
|
structure {'entity_id': [list of states], 'entity_id2': [list of states]}
|
||||||
|
|
||||||
|
We also need to go back and create a synthetic zero data point for
|
||||||
|
each list of states, otherwise our graphs won't start on the Y
|
||||||
|
axis correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = defaultdict(list)
|
||||||
|
|
||||||
|
entity_ids = [entity_id] if entity_id is not None else None
|
||||||
|
|
||||||
|
# Get the states at the start time
|
||||||
|
for state in get_states(start_time, entity_ids):
|
||||||
|
state.last_changed = start_time
|
||||||
|
state.last_updated = start_time
|
||||||
|
result[state.entity_id].append(state)
|
||||||
|
|
||||||
|
# Append all changes to it
|
||||||
|
for entity_id, group in groupby(states, lambda state: state.entity_id):
|
||||||
|
result[entity_id].extend(group)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_state(utc_point_in_time, entity_id, run=None):
|
def get_state(utc_point_in_time, entity_id, run=None):
|
||||||
""" Return a state at a specific point in time. """
|
""" Return a state at a specific point in time. """
|
||||||
states = get_states(utc_point_in_time, (entity_id,), run)
|
states = get_states(utc_point_in_time, (entity_id,), run)
|
||||||
@ -152,4 +199,4 @@ def _api_history_period(handler, path_match, data):
|
|||||||
entity_id = data.get('filter_entity_id')
|
entity_id = data.get('filter_entity_id')
|
||||||
|
|
||||||
handler.write_json(
|
handler.write_json(
|
||||||
state_changes_during_period(start_time, end_time, entity_id).values())
|
get_significant_states(start_time, end_time, entity_id).values())
|
||||||
|
@ -9,10 +9,8 @@ https://home-assistant.io/components/influxdb/
|
|||||||
import logging
|
import logging
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.helpers import validate_config
|
from homeassistant.helpers import validate_config
|
||||||
from homeassistant.const import (EVENT_STATE_CHANGED, STATE_ON, STATE_OFF,
|
from homeassistant.helpers import state as state_helper
|
||||||
STATE_UNLOCKED, STATE_LOCKED, STATE_UNKNOWN)
|
from homeassistant.const import (EVENT_STATE_CHANGED, STATE_UNKNOWN)
|
||||||
from homeassistant.components.sun import (STATE_ABOVE_HORIZON,
|
|
||||||
STATE_BELOW_HORIZON)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -22,14 +20,18 @@ DEPENDENCIES = []
|
|||||||
DEFAULT_HOST = 'localhost'
|
DEFAULT_HOST = 'localhost'
|
||||||
DEFAULT_PORT = 8086
|
DEFAULT_PORT = 8086
|
||||||
DEFAULT_DATABASE = 'home_assistant'
|
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_HOST = 'host'
|
||||||
CONF_PORT = 'port'
|
CONF_PORT = 'port'
|
||||||
CONF_DB_NAME = 'database'
|
CONF_DB_NAME = 'database'
|
||||||
CONF_USERNAME = 'username'
|
CONF_USERNAME = 'username'
|
||||||
CONF_PASSWORD = 'password'
|
CONF_PASSWORD = 'password'
|
||||||
|
CONF_SSL = 'ssl'
|
||||||
|
CONF_VERIFY_SSL = 'verify_ssl'
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
@ -37,7 +39,9 @@ def setup(hass, config):
|
|||||||
|
|
||||||
from influxdb import InfluxDBClient, exceptions
|
from influxdb import InfluxDBClient, exceptions
|
||||||
|
|
||||||
if not validate_config(config, {DOMAIN: ['host']}, _LOGGER):
|
if not validate_config(config, {DOMAIN: ['host',
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_PASSWORD]}, _LOGGER):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
conf = config[DOMAIN]
|
conf = config[DOMAIN]
|
||||||
@ -47,10 +51,14 @@ def setup(hass, config):
|
|||||||
database = util.convert(conf.get(CONF_DB_NAME), str, DEFAULT_DATABASE)
|
database = util.convert(conf.get(CONF_DB_NAME), str, DEFAULT_DATABASE)
|
||||||
username = util.convert(conf.get(CONF_USERNAME), str)
|
username = util.convert(conf.get(CONF_USERNAME), str)
|
||||||
password = util.convert(conf.get(CONF_PASSWORD), str)
|
password = util.convert(conf.get(CONF_PASSWORD), str)
|
||||||
|
ssl = util.convert(conf.get(CONF_SSL), bool, DEFAULT_SSL)
|
||||||
|
verify_ssl = util.convert(conf.get(CONF_VERIFY_SSL), bool,
|
||||||
|
DEFAULT_VERIFY_SSL)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
influx = InfluxDBClient(host=host, port=port, username=username,
|
influx = InfluxDBClient(host=host, port=port, username=username,
|
||||||
password=password, database=database)
|
password=password, database=database,
|
||||||
|
ssl=ssl, verify_ssl=verify_ssl)
|
||||||
influx.query("select * from /.*/ LIMIT 1;")
|
influx.query("select * from /.*/ LIMIT 1;")
|
||||||
except exceptions.InfluxDBClientError as exc:
|
except exceptions.InfluxDBClientError as exc:
|
||||||
_LOGGER.error("Database host is not accessible due to '%s', please "
|
_LOGGER.error("Database host is not accessible due to '%s', please "
|
||||||
@ -62,25 +70,17 @@ def setup(hass, config):
|
|||||||
""" Listen for new messages on the bus and sends them to Influx. """
|
""" Listen for new messages on the bus and sends them to Influx. """
|
||||||
|
|
||||||
state = event.data.get('new_state')
|
state = event.data.get('new_state')
|
||||||
|
if state is None or state.state in (STATE_UNKNOWN, ''):
|
||||||
if state is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON):
|
try:
|
||||||
_state = 1
|
_state = state_helper.state_as_number(state)
|
||||||
elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_UNKNOWN,
|
except ValueError:
|
||||||
STATE_BELOW_HORIZON):
|
|
||||||
_state = 0
|
|
||||||
else:
|
|
||||||
_state = state.state
|
_state = state.state
|
||||||
if _state == '':
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
_state = float(_state)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
measurement = state.attributes.get('unit_of_measurement', state.domain)
|
measurement = state.attributes.get('unit_of_measurement')
|
||||||
|
if measurement in (None, ''):
|
||||||
|
measurement = state.entity_id
|
||||||
|
|
||||||
json_body = [
|
json_body = [
|
||||||
{
|
{
|
||||||
|
@ -41,7 +41,7 @@ def turn_off(hass, entity_id):
|
|||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Set up input booleans."""
|
""" Set up input boolean. """
|
||||||
if not isinstance(config.get(DOMAIN), dict):
|
if not isinstance(config.get(DOMAIN), dict):
|
||||||
_LOGGER.error('Expected %s config to be a dictionary', DOMAIN)
|
_LOGGER.error('Expected %s config to be a dictionary', DOMAIN)
|
||||||
return False
|
return False
|
||||||
@ -68,7 +68,7 @@ def setup(hass, config):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def toggle_service(service):
|
def toggle_service(service):
|
||||||
"""Handle a calls to the input boolean services."""
|
""" Handle a calls to the input boolean services. """
|
||||||
target_inputs = component.extract_from_service(service)
|
target_inputs = component.extract_from_service(service)
|
||||||
|
|
||||||
for input_b in target_inputs:
|
for input_b in target_inputs:
|
||||||
@ -86,10 +86,10 @@ def setup(hass, config):
|
|||||||
|
|
||||||
|
|
||||||
class InputBoolean(ToggleEntity):
|
class InputBoolean(ToggleEntity):
|
||||||
"""Represent a boolean input within Home Assistant."""
|
""" Represent a boolean input. """
|
||||||
|
|
||||||
def __init__(self, object_id, name, state, icon):
|
def __init__(self, object_id, name, state, icon):
|
||||||
"""Initialize a boolean input."""
|
""" Initialize a boolean input. """
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state = state
|
self._state = state
|
||||||
|
140
homeassistant/components/input_select.py
Normal file
140
homeassistant/components/input_select.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.input_select
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Component to offer a way to select an option from a list.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation
|
||||||
|
at https://home-assistant.io/components/input_select/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
|
DOMAIN = 'input_select'
|
||||||
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_NAME = 'name'
|
||||||
|
CONF_INITIAL = 'initial'
|
||||||
|
CONF_ICON = 'icon'
|
||||||
|
CONF_OPTIONS = 'options'
|
||||||
|
|
||||||
|
ATTR_OPTION = 'option'
|
||||||
|
ATTR_OPTIONS = 'options'
|
||||||
|
|
||||||
|
SERVICE_SELECT_OPTION = 'select_option'
|
||||||
|
|
||||||
|
|
||||||
|
def select_option(hass, entity_id, option):
|
||||||
|
""" Set input_select to False. """
|
||||||
|
hass.services.call(DOMAIN, SERVICE_SELECT_OPTION, {
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_OPTION: option,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
""" Set up input select. """
|
||||||
|
if not isinstance(config.get(DOMAIN), dict):
|
||||||
|
_LOGGER.error('Expected %s config to be a dictionary', DOMAIN)
|
||||||
|
return False
|
||||||
|
|
||||||
|
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||||
|
|
||||||
|
entities = []
|
||||||
|
|
||||||
|
for object_id, cfg in config[DOMAIN].items():
|
||||||
|
if object_id != slugify(object_id):
|
||||||
|
_LOGGER.warning("Found invalid key for boolean input: %s. "
|
||||||
|
"Use %s instead", object_id, slugify(object_id))
|
||||||
|
continue
|
||||||
|
if not cfg:
|
||||||
|
_LOGGER.warning("No configuration specified for %s", object_id)
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = cfg.get(CONF_NAME)
|
||||||
|
options = cfg.get(CONF_OPTIONS)
|
||||||
|
|
||||||
|
if not isinstance(options, list) or len(options) == 0:
|
||||||
|
_LOGGER.warning('Key %s should be a list of options', CONF_OPTIONS)
|
||||||
|
continue
|
||||||
|
|
||||||
|
options = [str(val) for val in options]
|
||||||
|
|
||||||
|
state = cfg.get(CONF_INITIAL)
|
||||||
|
|
||||||
|
if state not in options:
|
||||||
|
state = options[0]
|
||||||
|
|
||||||
|
icon = cfg.get(CONF_ICON)
|
||||||
|
|
||||||
|
entities.append(InputSelect(object_id, name, state, options, icon))
|
||||||
|
|
||||||
|
if not entities:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def select_option_service(call):
|
||||||
|
""" Handle a calls to the input select services. """
|
||||||
|
target_inputs = component.extract_from_service(call)
|
||||||
|
|
||||||
|
for input_select in target_inputs:
|
||||||
|
input_select.select_option(call.data.get(ATTR_OPTION))
|
||||||
|
|
||||||
|
hass.services.register(DOMAIN, SERVICE_SELECT_OPTION,
|
||||||
|
select_option_service)
|
||||||
|
|
||||||
|
component.add_entities(entities)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class InputSelect(Entity):
|
||||||
|
""" Represent a select input. """
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
def __init__(self, object_id, name, state, options, icon):
|
||||||
|
""" Initialize a select input. """
|
||||||
|
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||||
|
self._name = name
|
||||||
|
self._current_option = state
|
||||||
|
self._options = options
|
||||||
|
self._icon = icon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
""" If entity should be polled. """
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Name of the select input. """
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
""" Icon to be used for this entity. """
|
||||||
|
return self._icon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" State of the component. """
|
||||||
|
return self._current_option
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_attributes(self):
|
||||||
|
""" State attributes. """
|
||||||
|
return {
|
||||||
|
ATTR_OPTIONS: self._options,
|
||||||
|
}
|
||||||
|
|
||||||
|
def select_option(self, option):
|
||||||
|
""" Select new option. """
|
||||||
|
if option not in self._options:
|
||||||
|
_LOGGER.warning('Invalid option: %s (possible options: %s)',
|
||||||
|
option, ', '.join(self._options))
|
||||||
|
return
|
||||||
|
self._current_option = option
|
||||||
|
self.update_ha_state()
|
@ -1,10 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
homeassistant.components.insteon
|
homeassistant.components.insteon_hub
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Support for Insteon Hub.
|
Support for Insteon Hub.
|
||||||
|
|
||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/insteon/
|
https://home-assistant.io/components/insteon_hub/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import homeassistant.bootstrap as bootstrap
|
import homeassistant.bootstrap as bootstrap
|
||||||
|
@ -11,7 +11,7 @@ import os
|
|||||||
import csv
|
import csv
|
||||||
|
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
group, discovery, wink, isy994, zwave, insteon_hub)
|
group, discovery, wink, isy994, zwave, insteon_hub, mysensors)
|
||||||
from homeassistant.config import load_yaml_config_file
|
from homeassistant.config import load_yaml_config_file
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
||||||
@ -64,6 +64,7 @@ DISCOVERY_PLATFORMS = {
|
|||||||
isy994.DISCOVER_LIGHTS: 'isy994',
|
isy994.DISCOVER_LIGHTS: 'isy994',
|
||||||
discovery.SERVICE_HUE: 'hue',
|
discovery.SERVICE_HUE: 'hue',
|
||||||
zwave.DISCOVER_LIGHTS: 'zwave',
|
zwave.DISCOVER_LIGHTS: 'zwave',
|
||||||
|
mysensors.DISCOVER_LIGHTS: 'mysensors',
|
||||||
}
|
}
|
||||||
|
|
||||||
PROP_TO_ATTR = {
|
PROP_TO_ATTR = {
|
||||||
@ -300,11 +301,6 @@ class Light(ToggleEntity):
|
|||||||
""" CT color value in mirads. """
|
""" CT color value in mirads. """
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
|
||||||
def device_state_attributes(self):
|
|
||||||
""" Returns device specific state attributes. """
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
""" Returns optional state attributes. """
|
""" Returns optional state attributes. """
|
||||||
@ -322,9 +318,4 @@ class Light(ToggleEntity):
|
|||||||
data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1],
|
data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1],
|
||||||
data[ATTR_BRIGHTNESS])
|
data[ATTR_BRIGHTNESS])
|
||||||
|
|
||||||
device_attr = self.device_state_attributes
|
|
||||||
|
|
||||||
if device_attr is not None:
|
|
||||||
data.update(device_attr)
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -16,7 +16,7 @@ from homeassistant.components.light import \
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['liffylights==0.9.0']
|
REQUIREMENTS = ['liffylights==0.9.4']
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
|
|
||||||
CONF_SERVER = "server" # server address configuration item
|
CONF_SERVER = "server" # server address configuration item
|
||||||
@ -58,10 +58,18 @@ class LIFX():
|
|||||||
bulb = self.find_bulb(ipaddr)
|
bulb = self.find_bulb(ipaddr)
|
||||||
|
|
||||||
if bulb is None:
|
if bulb is None:
|
||||||
|
_LOGGER.debug("new bulb %s %s %d %d %d %d %d",
|
||||||
|
ipaddr, name, power, hue, sat, bri, kel)
|
||||||
bulb = LIFXLight(self._liffylights, ipaddr, name,
|
bulb = LIFXLight(self._liffylights, ipaddr, name,
|
||||||
power, hue, sat, bri, kel)
|
power, hue, sat, bri, kel)
|
||||||
self._devices.append(bulb)
|
self._devices.append(bulb)
|
||||||
self._add_devices_callback([bulb])
|
self._add_devices_callback([bulb])
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("update bulb %s %s %d %d %d %d %d",
|
||||||
|
ipaddr, name, power, hue, sat, bri, kel)
|
||||||
|
bulb.set_power(power)
|
||||||
|
bulb.set_color(hue, sat, bri, kel)
|
||||||
|
bulb.update_ha_state()
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
def on_color(self, ipaddr, hue, sat, bri, kel):
|
def on_color(self, ipaddr, hue, sat, bri, kel):
|
||||||
@ -95,7 +103,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
lifx_library = LIFX(add_devices_callback, server_addr, broadcast_addr)
|
lifx_library = LIFX(add_devices_callback, server_addr, broadcast_addr)
|
||||||
|
|
||||||
# register our poll service
|
# register our poll service
|
||||||
track_time_change(hass, lifx_library.poll, second=10)
|
track_time_change(hass, lifx_library.poll, second=[10, 40])
|
||||||
|
|
||||||
lifx_library.probe()
|
lifx_library.probe()
|
||||||
|
|
||||||
|
226
homeassistant/components/light/mysensors.py
Normal file
226
homeassistant/components/light/mysensors.py
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.light.mysensors.
|
||||||
|
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Support for MySensors lights.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/light.mysensors.html
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.light import (
|
||||||
|
Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR)
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_BATTERY_LEVEL,
|
||||||
|
STATE_ON, STATE_OFF)
|
||||||
|
|
||||||
|
import homeassistant.components.mysensors as mysensors
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
ATTR_RGB_WHITE = 'rgb_white'
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the mysensors platform for sensors."""
|
||||||
|
# Only act if loaded via mysensors by discovery event.
|
||||||
|
# Otherwise gateway is not setup.
|
||||||
|
if discovery_info is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
for gateway in mysensors.GATEWAYS.values():
|
||||||
|
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||||
|
# states. Map them in a dict of lists.
|
||||||
|
pres = gateway.const.Presentation
|
||||||
|
set_req = gateway.const.SetReq
|
||||||
|
map_sv_types = {
|
||||||
|
pres.S_LIGHT: [set_req.V_LIGHT],
|
||||||
|
pres.S_DIMMER: [set_req.V_DIMMER],
|
||||||
|
}
|
||||||
|
if float(gateway.version) >= 1.5:
|
||||||
|
# Add V_RGBW when rgb_white is implemented in the frontend
|
||||||
|
map_sv_types.update({
|
||||||
|
pres.S_RGB_LIGHT: [set_req.V_RGB],
|
||||||
|
})
|
||||||
|
map_sv_types[pres.S_LIGHT].append(set_req.V_STATUS)
|
||||||
|
map_sv_types[pres.S_DIMMER].append(set_req.V_PERCENTAGE)
|
||||||
|
|
||||||
|
devices = {}
|
||||||
|
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||||
|
map_sv_types, devices, add_devices, MySensorsLight))
|
||||||
|
|
||||||
|
|
||||||
|
class MySensorsLight(Light):
|
||||||
|
"""Represent the value of a MySensors child node."""
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments,too-many-instance-attributes
|
||||||
|
|
||||||
|
def __init__(self, gateway, node_id, child_id, name, value_type):
|
||||||
|
"""Setup instance attributes."""
|
||||||
|
self.gateway = gateway
|
||||||
|
self.node_id = node_id
|
||||||
|
self.child_id = child_id
|
||||||
|
self._name = name
|
||||||
|
self.value_type = value_type
|
||||||
|
self.battery_level = 0
|
||||||
|
self._values = {}
|
||||||
|
self._state = None
|
||||||
|
self._rgb = None
|
||||||
|
self._brightness = None
|
||||||
|
self._white = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""MySensor gateway pushes its state to HA."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""The name of this entity."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
"""Brightness of this light between 0..255."""
|
||||||
|
return self._brightness
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rgb_color(self):
|
||||||
|
"""RGB color value [int, int, int]."""
|
||||||
|
return self._rgb
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rgb_white(self): # not implemented in the frontend yet
|
||||||
|
"""White value in RGBW, value between 0..255."""
|
||||||
|
return self._white
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return device specific state attributes."""
|
||||||
|
device_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,
|
||||||
|
}
|
||||||
|
for value_type, value in self._values.items():
|
||||||
|
device_attr[self.gateway.const.SetReq(value_type).name] = value
|
||||||
|
return device_attr
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""True if device is on."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
"""Turn the device on."""
|
||||||
|
set_req = self.gateway.const.SetReq
|
||||||
|
rgb = self._rgb
|
||||||
|
brightness = self._brightness
|
||||||
|
white = self._white
|
||||||
|
|
||||||
|
if set_req.V_LIGHT in self._values and not self._state:
|
||||||
|
self.gateway.set_child_value(
|
||||||
|
self.node_id, self.child_id, set_req.V_LIGHT, 1)
|
||||||
|
|
||||||
|
if ATTR_BRIGHTNESS in kwargs and set_req.V_DIMMER in self._values and \
|
||||||
|
kwargs[ATTR_BRIGHTNESS] != self._brightness:
|
||||||
|
brightness = kwargs[ATTR_BRIGHTNESS]
|
||||||
|
percent = round(100 * brightness / 255)
|
||||||
|
self.gateway.set_child_value(
|
||||||
|
self.node_id, self.child_id, set_req.V_DIMMER, percent)
|
||||||
|
|
||||||
|
if float(self.gateway.version) >= 1.5:
|
||||||
|
|
||||||
|
if ATTR_RGB_WHITE in kwargs and \
|
||||||
|
self.value_type in (set_req.V_RGB, set_req.V_RGBW) and \
|
||||||
|
kwargs[ATTR_RGB_WHITE] != self._white:
|
||||||
|
white = kwargs[ATTR_RGB_WHITE]
|
||||||
|
|
||||||
|
if ATTR_RGB_COLOR in kwargs and \
|
||||||
|
self.value_type in (set_req.V_RGB, set_req.V_RGBW) and \
|
||||||
|
kwargs[ATTR_RGB_COLOR] != self._rgb:
|
||||||
|
rgb = kwargs[ATTR_RGB_COLOR]
|
||||||
|
if set_req.V_RGBW == self.value_type:
|
||||||
|
hex_template = '%02x%02x%02x%02x'
|
||||||
|
color_list = rgb.append(white)
|
||||||
|
if set_req.V_RGB == self.value_type:
|
||||||
|
hex_template = '%02x%02x%02x'
|
||||||
|
color_list = rgb
|
||||||
|
hex_color = hex_template % tuple(color_list)
|
||||||
|
self.gateway.set_child_value(
|
||||||
|
self.node_id, self.child_id, self.value_type, hex_color)
|
||||||
|
|
||||||
|
if self.gateway.optimistic:
|
||||||
|
# optimistically assume that light has changed state
|
||||||
|
self._state = True
|
||||||
|
self._rgb = rgb
|
||||||
|
self._brightness = brightness
|
||||||
|
self._white = white
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
"""Turn the device off."""
|
||||||
|
set_req = self.gateway.const.SetReq
|
||||||
|
v_type = set_req.V_LIGHT
|
||||||
|
value = 0
|
||||||
|
if set_req.V_LIGHT in self._values:
|
||||||
|
self._values[set_req.V_LIGHT] = STATE_OFF
|
||||||
|
elif set_req.V_DIMMER in self._values:
|
||||||
|
v_type = set_req.V_DIMMER
|
||||||
|
elif float(self.gateway.version) >= 1.5:
|
||||||
|
if set_req.V_RGB in self._values:
|
||||||
|
v_type = set_req.V_RGB
|
||||||
|
value = '000000'
|
||||||
|
elif set_req.V_RGBW in self._values:
|
||||||
|
v_type = set_req.V_RGBW
|
||||||
|
value = '00000000'
|
||||||
|
self.gateway.set_child_value(
|
||||||
|
self.node_id, self.child_id, v_type, value)
|
||||||
|
|
||||||
|
if self.gateway.optimistic:
|
||||||
|
# optimistically assume that light has changed state
|
||||||
|
self._state = False
|
||||||
|
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]
|
||||||
|
child = node.children[self.child_id]
|
||||||
|
set_req = self.gateway.const.SetReq
|
||||||
|
self.battery_level = node.battery_level
|
||||||
|
for value_type, value in child.values.items():
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s: value_type %s, value = %s", self._name, value_type, value)
|
||||||
|
if value_type == set_req.V_LIGHT:
|
||||||
|
self._values[value_type] = (
|
||||||
|
STATE_ON if int(value) == 1 else STATE_OFF)
|
||||||
|
self._state = self._values[value_type] == STATE_ON
|
||||||
|
else:
|
||||||
|
self._values[value_type] = value
|
||||||
|
if value_type == set_req.V_DIMMER:
|
||||||
|
self._brightness = round(
|
||||||
|
255 * int(self._values[value_type]) / 100)
|
||||||
|
if self._brightness == 0:
|
||||||
|
self._state = False
|
||||||
|
if set_req.V_LIGHT not in self._values:
|
||||||
|
self._state = self._brightness > 0
|
||||||
|
if float(self.gateway.version) >= 1.5 and \
|
||||||
|
value_type in (set_req.V_RGB, set_req.V_RGBW):
|
||||||
|
# convert hex color string to rgb(w) integer list
|
||||||
|
color_list = [int(value[i:i + len(value) // 3], 16)
|
||||||
|
for i in range(0,
|
||||||
|
len(value),
|
||||||
|
len(value) // 3)]
|
||||||
|
if len(color_list) > 3:
|
||||||
|
self._white = color_list.pop()
|
||||||
|
self._rgb = color_list
|
||||||
|
if set_req.V_LIGHT not in self._values or \
|
||||||
|
set_req.V_DIMMER not in self._values:
|
||||||
|
self._state = max(color_list) > 0
|
@ -50,7 +50,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
|
|
||||||
def light_update(event):
|
def light_update(event):
|
||||||
""" Callback for light updates from the RFXtrx gateway. """
|
""" Callback for light updates from the RFXtrx gateway. """
|
||||||
if not isinstance(event.device, rfxtrxmod.LightingDevice):
|
if not isinstance(event.device, rfxtrxmod.LightingDevice) or \
|
||||||
|
not event.device.known_to_be_dimmable:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Add entity if not exist and the automatic_add is True
|
# Add entity if not exist and the automatic_add is True
|
||||||
@ -74,13 +75,13 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
add_devices_callback([new_light])
|
add_devices_callback([new_light])
|
||||||
|
|
||||||
# Check if entity exists or previously added automatically
|
# Check if entity exists or previously added automatically
|
||||||
if entity_id in rfxtrx.RFX_DEVICES \
|
if entity_id in rfxtrx.RFX_DEVICES:
|
||||||
and isinstance(rfxtrx.RFX_DEVICES[entity_id], RfxtrxLight):
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"EntityID: %s light_update. Command: %s",
|
"EntityID: %s light_update. Command: %s",
|
||||||
entity_id,
|
entity_id,
|
||||||
event.values['Command']
|
event.values['Command']
|
||||||
)
|
)
|
||||||
|
|
||||||
if event.values['Command'] == 'On'\
|
if event.values['Command'] == 'On'\
|
||||||
or event.values['Command'] == 'Off':
|
or event.values['Command'] == 'Off':
|
||||||
|
|
||||||
@ -90,15 +91,27 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
rfxtrx.RFX_DEVICES[entity_id]._state = is_on
|
rfxtrx.RFX_DEVICES[entity_id]._state = is_on
|
||||||
rfxtrx.RFX_DEVICES[entity_id].update_ha_state()
|
rfxtrx.RFX_DEVICES[entity_id].update_ha_state()
|
||||||
|
|
||||||
# Fire event
|
elif event.values['Command'] == 'Set level':
|
||||||
if rfxtrx.RFX_DEVICES[entity_id].should_fire_event:
|
# pylint: disable=protected-access
|
||||||
rfxtrx.RFX_DEVICES[entity_id].hass.bus.fire(
|
rfxtrx.RFX_DEVICES[entity_id]._brightness = \
|
||||||
EVENT_BUTTON_PRESSED, {
|
(event.values['Dim level'] * 255 // 100)
|
||||||
ATTR_ENTITY_ID:
|
|
||||||
rfxtrx.RFX_DEVICES[entity_id].entity_id,
|
# Update the rfxtrx device state
|
||||||
ATTR_STATE: event.values['Command'].lower()
|
is_on = rfxtrx.RFX_DEVICES[entity_id]._brightness > 0
|
||||||
}
|
rfxtrx.RFX_DEVICES[entity_id]._state = is_on
|
||||||
)
|
rfxtrx.RFX_DEVICES[entity_id].update_ha_state()
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fire event
|
||||||
|
if rfxtrx.RFX_DEVICES[entity_id].should_fire_event:
|
||||||
|
rfxtrx.RFX_DEVICES[entity_id].hass.bus.fire(
|
||||||
|
EVENT_BUTTON_PRESSED, {
|
||||||
|
ATTR_ENTITY_ID:
|
||||||
|
rfxtrx.RFX_DEVICES[entity_id].entity_id,
|
||||||
|
ATTR_STATE: event.values['Command'].lower()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Subscribe to main rfxtrx events
|
# Subscribe to main rfxtrx events
|
||||||
if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
||||||
|
118
homeassistant/components/light/scsgate.py
Normal file
118
homeassistant/components/light/scsgate.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.light.scsgate
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Support for SCSGate lights.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/light.scsgate/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import homeassistant.components.scsgate as scsgate
|
||||||
|
|
||||||
|
from homeassistant.components.light import Light
|
||||||
|
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID
|
||||||
|
|
||||||
|
DEPENDENCIES = ['scsgate']
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
|
""" Add the SCSGate swiches defined inside of the configuration file. """
|
||||||
|
|
||||||
|
devices = config.get('devices')
|
||||||
|
lights = []
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if devices:
|
||||||
|
for _, entity_info in devices.items():
|
||||||
|
if entity_info['scs_id'] in scsgate.SCSGATE.devices:
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info("Adding %s scsgate.light", entity_info['name'])
|
||||||
|
|
||||||
|
name = entity_info['name']
|
||||||
|
scs_id = entity_info['scs_id']
|
||||||
|
light = SCSGateLight(
|
||||||
|
name=name,
|
||||||
|
scs_id=scs_id,
|
||||||
|
logger=logger)
|
||||||
|
lights.append(light)
|
||||||
|
|
||||||
|
add_devices_callback(lights)
|
||||||
|
scsgate.SCSGATE.add_devices_to_register(lights)
|
||||||
|
|
||||||
|
|
||||||
|
class SCSGateLight(Light):
|
||||||
|
""" Provides a SCSGate light. """
|
||||||
|
def __init__(self, scs_id, name, logger):
|
||||||
|
self._name = name
|
||||||
|
self._scs_id = scs_id
|
||||||
|
self._toggled = False
|
||||||
|
self._logger = logger
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scs_id(self):
|
||||||
|
""" SCS ID """
|
||||||
|
return self._scs_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
""" No polling needed for a SCSGate light. """
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Returns the name of the device if any. """
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
""" True if light is on. """
|
||||||
|
return self._toggled
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
""" Turn the device on. """
|
||||||
|
from scsgate.tasks import ToggleStatusTask
|
||||||
|
|
||||||
|
scsgate.SCSGATE.append_task(
|
||||||
|
ToggleStatusTask(
|
||||||
|
target=self._scs_id,
|
||||||
|
toggled=True))
|
||||||
|
|
||||||
|
self._toggled = True
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
""" Turn the device off. """
|
||||||
|
from scsgate.tasks import ToggleStatusTask
|
||||||
|
|
||||||
|
scsgate.SCSGATE.append_task(
|
||||||
|
ToggleStatusTask(
|
||||||
|
target=self._scs_id,
|
||||||
|
toggled=False))
|
||||||
|
|
||||||
|
self._toggled = False
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
|
def process_event(self, message):
|
||||||
|
""" Handle a SCSGate message related with this light """
|
||||||
|
if self._toggled == message.toggled:
|
||||||
|
self._logger.info(
|
||||||
|
"Light %s, ignoring message %s because state already active",
|
||||||
|
self._scs_id, message)
|
||||||
|
# Nothing changed, ignoring
|
||||||
|
return
|
||||||
|
|
||||||
|
self._toggled = message.toggled
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
|
command = "off"
|
||||||
|
if self._toggled:
|
||||||
|
command = "on"
|
||||||
|
|
||||||
|
self.hass.bus.fire(
|
||||||
|
'button_pressed', {
|
||||||
|
ATTR_ENTITY_ID: self._scs_id,
|
||||||
|
'state': command
|
||||||
|
}
|
||||||
|
)
|
@ -9,13 +9,20 @@ https://home-assistant.io/components/light.vera/
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
from homeassistant.components.switch.vera import VeraSwitch
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
|
||||||
|
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_ON
|
from homeassistant.const import (
|
||||||
|
ATTR_BATTERY_LEVEL,
|
||||||
|
ATTR_TRIPPED,
|
||||||
|
ATTR_ARMED,
|
||||||
|
ATTR_LAST_TRIP_TIME,
|
||||||
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
|
STATE_ON,
|
||||||
|
STATE_OFF)
|
||||||
|
|
||||||
REQUIREMENTS = ['pyvera==0.2.7']
|
REQUIREMENTS = ['pyvera==0.2.8']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -67,17 +74,35 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
add_devices_callback(lights)
|
add_devices_callback(lights)
|
||||||
|
|
||||||
|
|
||||||
class VeraLight(VeraSwitch):
|
class VeraLight(Light):
|
||||||
""" Represents a Vera Light, including dimmable. """
|
""" Represents a Vera Light, including dimmable. """
|
||||||
|
|
||||||
|
def __init__(self, vera_device, controller, extra_data=None):
|
||||||
|
self.vera_device = vera_device
|
||||||
|
self.extra_data = extra_data
|
||||||
|
self.controller = controller
|
||||||
|
if self.extra_data and self.extra_data.get('name'):
|
||||||
|
self._name = self.extra_data.get('name')
|
||||||
|
else:
|
||||||
|
self._name = self.vera_device.name
|
||||||
|
self._state = STATE_OFF
|
||||||
|
|
||||||
|
self.controller.register(vera_device, self._update_callback)
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def _update_callback(self, _device):
|
||||||
|
self.update_ha_state(True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def name(self):
|
||||||
attr = super().state_attributes or {}
|
""" Get the mame of the switch. """
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
"""Brightness of the light."""
|
||||||
if self.vera_device.is_dimmable:
|
if self.vera_device.is_dimmable:
|
||||||
attr[ATTR_BRIGHTNESS] = self.vera_device.get_brightness()
|
return self.vera_device.get_brightness()
|
||||||
|
|
||||||
return attr
|
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
if ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable:
|
if ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable:
|
||||||
@ -87,3 +112,49 @@ class VeraLight(VeraSwitch):
|
|||||||
|
|
||||||
self._state = STATE_ON
|
self._state = STATE_ON
|
||||||
self.update_ha_state(True)
|
self.update_ha_state(True)
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
self.vera_device.switch_off()
|
||||||
|
self._state = STATE_OFF
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
attr = {}
|
||||||
|
|
||||||
|
if self.vera_device.has_battery:
|
||||||
|
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
||||||
|
|
||||||
|
if self.vera_device.is_armable:
|
||||||
|
armed = self.vera_device.is_armed
|
||||||
|
attr[ATTR_ARMED] = 'True' if armed else 'False'
|
||||||
|
|
||||||
|
if self.vera_device.is_trippable:
|
||||||
|
last_tripped = self.vera_device.last_trip
|
||||||
|
if last_tripped is not None:
|
||||||
|
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
|
||||||
|
attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str(
|
||||||
|
utc_time)
|
||||||
|
else:
|
||||||
|
attr[ATTR_LAST_TRIP_TIME] = None
|
||||||
|
tripped = self.vera_device.is_tripped
|
||||||
|
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
|
||||||
|
|
||||||
|
attr['Vera Device Id'] = self.vera_device.vera_device_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
""" Tells Home Assistant not to poll this entity. """
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
""" True if device is on. """
|
||||||
|
return self._state == STATE_ON
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
""" Called by the vera device callback to update state. """
|
||||||
|
if self.vera_device.is_switched_on():
|
||||||
|
self._state = STATE_ON
|
||||||
|
else:
|
||||||
|
self._state = STATE_OFF
|
||||||
|
@ -8,11 +8,10 @@ https://home-assistant.io/components/light.wink/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
|
||||||
from homeassistant.components.wink import WinkToggleDevice
|
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
|
||||||
REQUIREMENTS = ['python-wink==0.4.2']
|
REQUIREMENTS = ['python-wink==0.5.0']
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
@ -34,9 +33,32 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
WinkLight(light) for light in pywink.get_bulbs())
|
WinkLight(light) for light in pywink.get_bulbs())
|
||||||
|
|
||||||
|
|
||||||
class WinkLight(WinkToggleDevice):
|
class WinkLight(Light):
|
||||||
""" Represents a Wink light. """
|
""" Represents a Wink light. """
|
||||||
|
|
||||||
|
def __init__(self, wink):
|
||||||
|
self.wink = wink
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
""" Returns the id of this Wink switch. """
|
||||||
|
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Returns the name of the light if any. """
|
||||||
|
return self.wink.name()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
""" True if light is on. """
|
||||||
|
return self.wink.state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
"""Brightness of the light."""
|
||||||
|
return int(self.wink.brightness() * 255)
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
""" Turns the switch on. """
|
""" Turns the switch on. """
|
||||||
@ -48,14 +70,10 @@ class WinkLight(WinkToggleDevice):
|
|||||||
else:
|
else:
|
||||||
self.wink.set_state(True)
|
self.wink.set_state(True)
|
||||||
|
|
||||||
@property
|
def turn_off(self):
|
||||||
def state_attributes(self):
|
""" Turns the switch off. """
|
||||||
attr = super().state_attributes
|
self.wink.set_state(False)
|
||||||
|
|
||||||
if self.is_on:
|
def update(self):
|
||||||
brightness = self.wink.brightness()
|
""" Update state of the light. """
|
||||||
|
self.wink.update_state()
|
||||||
if brightness is not None:
|
|
||||||
attr[ATTR_BRIGHTNESS] = int(brightness * 255)
|
|
||||||
|
|
||||||
return attr
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
homeassistant.components.light.zigbee
|
homeassistant.components.light.zigbee
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Contains functionality to use a ZigBee device as a light.
|
Contains functionality to use a ZigBee device as a light.
|
||||||
"""
|
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/light.zigbee/
|
||||||
|
"""
|
||||||
from homeassistant.components.light import Light
|
from homeassistant.components.light import Light
|
||||||
from homeassistant.components.zigbee import (
|
from homeassistant.components.zigbee import (
|
||||||
ZigBeeDigitalOut, ZigBeeDigitalOutConfig)
|
ZigBeeDigitalOut, ZigBeeDigitalOutConfig)
|
||||||
@ -13,9 +15,7 @@ DEPENDENCIES = ["zigbee"]
|
|||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""
|
""" Create and add an entity based on the configuration. """
|
||||||
Create and add an entity based on the configuration.
|
|
||||||
"""
|
|
||||||
add_entities([
|
add_entities([
|
||||||
ZigBeeLight(hass, ZigBeeDigitalOutConfig(config))
|
ZigBeeLight(hass, ZigBeeDigitalOutConfig(config))
|
||||||
])
|
])
|
||||||
|
@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK,
|
STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK,
|
||||||
ATTR_ENTITY_ID)
|
ATTR_ENTITY_ID)
|
||||||
from homeassistant.components import (group, wink)
|
from homeassistant.components import (group, verisure, wink)
|
||||||
|
|
||||||
DOMAIN = 'lock'
|
DOMAIN = 'lock'
|
||||||
SCAN_INTERVAL = 30
|
SCAN_INTERVAL = 30
|
||||||
@ -28,12 +28,15 @@ ENTITY_ID_ALL_LOCKS = group.ENTITY_ID_FORMAT.format('all_locks')
|
|||||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
|
|
||||||
ATTR_LOCKED = "locked"
|
ATTR_LOCKED = "locked"
|
||||||
|
ATTR_CODE = 'code'
|
||||||
|
ATTR_CODE_FORMAT = 'code_format'
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||||
|
|
||||||
# Maps discovered services to their platforms
|
# Maps discovered services to their platforms
|
||||||
DISCOVERY_PLATFORMS = {
|
DISCOVERY_PLATFORMS = {
|
||||||
wink.DISCOVER_LOCKS: 'wink'
|
wink.DISCOVER_LOCKS: 'wink',
|
||||||
|
verisure.DISCOVER_LOCKS: 'verisure'
|
||||||
}
|
}
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -45,15 +48,25 @@ def is_locked(hass, entity_id=None):
|
|||||||
return hass.states.is_state(entity_id, STATE_LOCKED)
|
return hass.states.is_state(entity_id, STATE_LOCKED)
|
||||||
|
|
||||||
|
|
||||||
def lock(hass, entity_id=None):
|
def lock(hass, entity_id=None, code=None):
|
||||||
""" Locks all or specified locks. """
|
""" Locks all or specified locks. """
|
||||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
data = {}
|
||||||
|
if code:
|
||||||
|
data[ATTR_CODE] = code
|
||||||
|
if entity_id:
|
||||||
|
data[ATTR_ENTITY_ID] = entity_id
|
||||||
|
|
||||||
hass.services.call(DOMAIN, SERVICE_LOCK, data)
|
hass.services.call(DOMAIN, SERVICE_LOCK, data)
|
||||||
|
|
||||||
|
|
||||||
def unlock(hass, entity_id=None):
|
def unlock(hass, entity_id=None, code=None):
|
||||||
""" Unlocks all or specified locks. """
|
""" Unlocks all or specified locks. """
|
||||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
data = {}
|
||||||
|
if code:
|
||||||
|
data[ATTR_CODE] = code
|
||||||
|
if entity_id:
|
||||||
|
data[ATTR_ENTITY_ID] = entity_id
|
||||||
|
|
||||||
hass.services.call(DOMAIN, SERVICE_UNLOCK, data)
|
hass.services.call(DOMAIN, SERVICE_UNLOCK, data)
|
||||||
|
|
||||||
|
|
||||||
@ -68,11 +81,16 @@ def setup(hass, config):
|
|||||||
""" Handles calls to the lock services. """
|
""" Handles calls to the lock services. """
|
||||||
target_locks = component.extract_from_service(service)
|
target_locks = component.extract_from_service(service)
|
||||||
|
|
||||||
|
if ATTR_CODE not in service.data:
|
||||||
|
code = None
|
||||||
|
else:
|
||||||
|
code = service.data[ATTR_CODE]
|
||||||
|
|
||||||
for item in target_locks:
|
for item in target_locks:
|
||||||
if service.service == SERVICE_LOCK:
|
if service.service == SERVICE_LOCK:
|
||||||
item.lock()
|
item.lock(code=code)
|
||||||
else:
|
else:
|
||||||
item.unlock()
|
item.unlock(code=code)
|
||||||
|
|
||||||
if item.should_poll:
|
if item.should_poll:
|
||||||
item.update_ha_state(True)
|
item.update_ha_state(True)
|
||||||
@ -91,19 +109,34 @@ class LockDevice(Entity):
|
|||||||
""" Represents a lock within Home Assistant. """
|
""" Represents a lock within Home Assistant. """
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code_format(self):
|
||||||
|
""" regex for code format or None if no code is required. """
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_locked(self):
|
def is_locked(self):
|
||||||
""" Is the lock locked or unlocked. """
|
""" Is the lock locked or unlocked. """
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def lock(self):
|
def lock(self, **kwargs):
|
||||||
""" Locks the lock. """
|
""" Locks the lock. """
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def unlock(self):
|
def unlock(self, **kwargs):
|
||||||
""" Unlocks the lock. """
|
""" Unlocks the lock. """
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_attributes(self):
|
||||||
|
""" Return the state attributes. """
|
||||||
|
if self.code_format is None:
|
||||||
|
return None
|
||||||
|
state_attr = {
|
||||||
|
ATTR_CODE_FORMAT: self.code_format,
|
||||||
|
}
|
||||||
|
return state_attr
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
locked = self.is_locked
|
locked = self.is_locked
|
||||||
|
92
homeassistant/components/lock/verisure.py
Normal file
92
homeassistant/components/lock/verisure.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.lock.verisure
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Interfaces with Verisure locks.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/verisure/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import homeassistant.components.verisure as verisure
|
||||||
|
from homeassistant.components.lock import LockDevice
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
STATE_LOCKED, STATE_UNLOCKED)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
ATTR_CODE = 'code'
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Sets up the Verisure platform. """
|
||||||
|
|
||||||
|
if not verisure.MY_PAGES:
|
||||||
|
_LOGGER.error('A connection has not been made to Verisure mypages.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
locks = []
|
||||||
|
|
||||||
|
locks.extend([VerisureDoorlock(value)
|
||||||
|
for value in verisure.LOCK_STATUS.values()
|
||||||
|
if verisure.SHOW_LOCKS])
|
||||||
|
|
||||||
|
add_devices(locks)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method
|
||||||
|
class VerisureDoorlock(LockDevice):
|
||||||
|
""" Represents a Verisure doorlock status. """
|
||||||
|
|
||||||
|
def __init__(self, lock_status, code=None):
|
||||||
|
self._id = lock_status.id
|
||||||
|
self._state = STATE_UNKNOWN
|
||||||
|
self._code = code
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Returns the name of the device. """
|
||||||
|
return 'Lock {}'.format(self._id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" Returns the state of the device. """
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code_format(self):
|
||||||
|
""" Six digit code required. """
|
||||||
|
return '^\\d{%s}$' % verisure.CODE_DIGITS
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
""" Update lock status """
|
||||||
|
verisure.update_lock()
|
||||||
|
|
||||||
|
if verisure.LOCK_STATUS[self._id].status == 'unlocked':
|
||||||
|
self._state = STATE_UNLOCKED
|
||||||
|
elif verisure.LOCK_STATUS[self._id].status == 'locked':
|
||||||
|
self._state = STATE_LOCKED
|
||||||
|
elif verisure.LOCK_STATUS[self._id].status != 'pending':
|
||||||
|
_LOGGER.error(
|
||||||
|
'Unknown lock state %s',
|
||||||
|
verisure.LOCK_STATUS[self._id].status)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_locked(self):
|
||||||
|
""" True if device is locked. """
|
||||||
|
return verisure.LOCK_STATUS[self._id].status
|
||||||
|
|
||||||
|
def unlock(self, **kwargs):
|
||||||
|
""" Send unlock command. """
|
||||||
|
verisure.MY_PAGES.lock.set(kwargs[ATTR_CODE], self._id, 'UNLOCKED')
|
||||||
|
_LOGGER.info('verisure doorlock unlocking')
|
||||||
|
verisure.MY_PAGES.lock.wait_while_pending()
|
||||||
|
verisure.update_lock()
|
||||||
|
|
||||||
|
def lock(self, **kwargs):
|
||||||
|
""" Send lock command. """
|
||||||
|
verisure.MY_PAGES.lock.set(kwargs[ATTR_CODE], self._id, 'LOCKED')
|
||||||
|
_LOGGER.info('verisure doorlock locking')
|
||||||
|
verisure.MY_PAGES.lock.wait_while_pending()
|
||||||
|
verisure.update_lock()
|
@ -11,7 +11,7 @@ import logging
|
|||||||
from homeassistant.components.lock import LockDevice
|
from homeassistant.components.lock import LockDevice
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
|
||||||
REQUIREMENTS = ['python-wink==0.4.2']
|
REQUIREMENTS = ['python-wink==0.5.0']
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
@ -32,7 +32,6 @@ DISCOVERY_PLATFORMS = {
|
|||||||
discovery.SERVICE_PLEX: 'plex',
|
discovery.SERVICE_PLEX: 'plex',
|
||||||
}
|
}
|
||||||
|
|
||||||
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
|
|
||||||
SERVICE_PLAY_MEDIA = 'play_media'
|
SERVICE_PLAY_MEDIA = 'play_media'
|
||||||
|
|
||||||
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
|
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
|
||||||
@ -68,14 +67,12 @@ SUPPORT_VOLUME_SET = 4
|
|||||||
SUPPORT_VOLUME_MUTE = 8
|
SUPPORT_VOLUME_MUTE = 8
|
||||||
SUPPORT_PREVIOUS_TRACK = 16
|
SUPPORT_PREVIOUS_TRACK = 16
|
||||||
SUPPORT_NEXT_TRACK = 32
|
SUPPORT_NEXT_TRACK = 32
|
||||||
SUPPORT_YOUTUBE = 64
|
|
||||||
SUPPORT_TURN_ON = 128
|
SUPPORT_TURN_ON = 128
|
||||||
SUPPORT_TURN_OFF = 256
|
SUPPORT_TURN_OFF = 256
|
||||||
SUPPORT_PLAY_MEDIA = 512
|
SUPPORT_PLAY_MEDIA = 512
|
||||||
SUPPORT_VOLUME_STEP = 1024
|
SUPPORT_VOLUME_STEP = 1024
|
||||||
|
|
||||||
YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg'
|
|
||||||
|
|
||||||
SERVICE_TO_METHOD = {
|
SERVICE_TO_METHOD = {
|
||||||
SERVICE_TURN_ON: 'turn_on',
|
SERVICE_TURN_ON: 'turn_on',
|
||||||
SERVICE_TURN_OFF: 'turn_off',
|
SERVICE_TURN_OFF: 'turn_off',
|
||||||
@ -200,6 +197,13 @@ def media_previous_track(hass, entity_id=None):
|
|||||||
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data)
|
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data)
|
||||||
|
|
||||||
|
|
||||||
|
def media_seek(hass, position, entity_id=None):
|
||||||
|
""" Send the media player the command to seek in current playing media. """
|
||||||
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||||
|
data[ATTR_MEDIA_SEEK_POSITION] = position
|
||||||
|
hass.services.call(DOMAIN, SERVICE_MEDIA_SEEK, data)
|
||||||
|
|
||||||
|
|
||||||
def play_media(hass, media_type, media_id, entity_id=None):
|
def play_media(hass, media_type, media_id, entity_id=None):
|
||||||
""" Send the media player the command for playing media. """
|
""" Send the media player the command for playing media. """
|
||||||
data = {"media_type": media_type, "media_id": media_id}
|
data = {"media_type": media_type, "media_id": media_id}
|
||||||
@ -283,7 +287,7 @@ def setup(hass, config):
|
|||||||
position = service.data[ATTR_MEDIA_SEEK_POSITION]
|
position = service.data[ATTR_MEDIA_SEEK_POSITION]
|
||||||
|
|
||||||
for player in target_players:
|
for player in target_players:
|
||||||
player.seek(position)
|
player.media_seek(position)
|
||||||
|
|
||||||
if player.should_poll:
|
if player.should_poll:
|
||||||
player.update_ha_state(True)
|
player.update_ha_state(True)
|
||||||
@ -291,20 +295,6 @@ def setup(hass, config):
|
|||||||
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service,
|
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service,
|
||||||
descriptions.get(SERVICE_MEDIA_SEEK))
|
descriptions.get(SERVICE_MEDIA_SEEK))
|
||||||
|
|
||||||
def play_youtube_video_service(service, media_id=None):
|
|
||||||
""" Plays specified media_id on the media player. """
|
|
||||||
if media_id is None:
|
|
||||||
service.data.get('video')
|
|
||||||
|
|
||||||
if media_id is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
for player in component.extract_from_service(service):
|
|
||||||
player.play_youtube(media_id)
|
|
||||||
|
|
||||||
if player.should_poll:
|
|
||||||
player.update_ha_state(True)
|
|
||||||
|
|
||||||
def play_media_service(service):
|
def play_media_service(service):
|
||||||
""" Plays specified media_id on the media player. """
|
""" Plays specified media_id on the media player. """
|
||||||
media_type = service.data.get('media_type')
|
media_type = service.data.get('media_type')
|
||||||
@ -322,20 +312,6 @@ def setup(hass, config):
|
|||||||
if player.should_poll:
|
if player.should_poll:
|
||||||
player.update_ha_state(True)
|
player.update_ha_state(True)
|
||||||
|
|
||||||
hass.services.register(
|
|
||||||
DOMAIN, "start_fireplace",
|
|
||||||
lambda service: play_youtube_video_service(service, "eyU3bRy2x44"),
|
|
||||||
descriptions.get('start_fireplace'))
|
|
||||||
|
|
||||||
hass.services.register(
|
|
||||||
DOMAIN, "start_epic_sax",
|
|
||||||
lambda service: play_youtube_video_service(service, "kxopViU98Xo"),
|
|
||||||
descriptions.get('start_epic_sax'))
|
|
||||||
|
|
||||||
hass.services.register(
|
|
||||||
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service,
|
|
||||||
descriptions.get(SERVICE_YOUTUBE_VIDEO))
|
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.register(
|
||||||
DOMAIN, SERVICE_PLAY_MEDIA, play_media_service,
|
DOMAIN, SERVICE_PLAY_MEDIA, play_media_service,
|
||||||
descriptions.get(SERVICE_PLAY_MEDIA))
|
descriptions.get(SERVICE_PLAY_MEDIA))
|
||||||
@ -449,11 +425,6 @@ class MediaPlayerDevice(Entity):
|
|||||||
""" Flags of media commands that are supported. """
|
""" Flags of media commands that are supported. """
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@property
|
|
||||||
def device_state_attributes(self):
|
|
||||||
""" Extra attributes a device wants to expose. """
|
|
||||||
return None
|
|
||||||
|
|
||||||
def turn_on(self):
|
def turn_on(self):
|
||||||
""" turn the media player on. """
|
""" turn the media player on. """
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@ -490,10 +461,6 @@ class MediaPlayerDevice(Entity):
|
|||||||
""" Send seek command. """
|
""" Send seek command. """
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def play_youtube(self, media_id):
|
|
||||||
""" Plays a YouTube media. """
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def play_media(self, media_type, media_id):
|
def play_media(self, media_type, media_id):
|
||||||
""" Plays a piece of media. """
|
""" Plays a piece of media. """
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@ -529,11 +496,6 @@ class MediaPlayerDevice(Entity):
|
|||||||
""" Boolean if next track command supported. """
|
""" Boolean if next track command supported. """
|
||||||
return bool(self.supported_media_commands & SUPPORT_NEXT_TRACK)
|
return bool(self.supported_media_commands & SUPPORT_NEXT_TRACK)
|
||||||
|
|
||||||
@property
|
|
||||||
def support_youtube(self):
|
|
||||||
""" Boolean if YouTube is supported. """
|
|
||||||
return bool(self.supported_media_commands & SUPPORT_YOUTUBE)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def support_play_media(self):
|
def support_play_media(self):
|
||||||
""" Boolean if play media command supported. """
|
""" Boolean if play media command supported. """
|
||||||
@ -579,9 +541,4 @@ class MediaPlayerDevice(Entity):
|
|||||||
if self.media_image_url:
|
if self.media_image_url:
|
||||||
state_attr[ATTR_ENTITY_PICTURE] = self.media_image_url
|
state_attr[ATTR_ENTITY_PICTURE] = self.media_image_url
|
||||||
|
|
||||||
device_attr = self.device_state_attributes
|
|
||||||
|
|
||||||
if device_attr:
|
|
||||||
state_attr.update(device_attr)
|
|
||||||
|
|
||||||
return state_attr
|
return state_attr
|
||||||
|
@ -16,7 +16,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
MediaPlayerDevice,
|
MediaPlayerDevice,
|
||||||
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
||||||
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_YOUTUBE, SUPPORT_PLAY_MEDIA,
|
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA,
|
||||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
|
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
|
||||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
|
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
|
||||||
|
|
||||||
@ -25,51 +25,48 @@ CONF_IGNORE_CEC = 'ignore_cec'
|
|||||||
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
|
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
|
||||||
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
|
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
|
||||||
SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE | SUPPORT_PLAY_MEDIA
|
SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA
|
||||||
KNOWN_HOSTS = []
|
KNOWN_HOSTS = []
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
DEFAULT_PORT = 8009
|
||||||
cast = None
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
""" Sets up the cast platform. """
|
""" Sets up the cast platform. """
|
||||||
global cast
|
|
||||||
import pychromecast
|
import pychromecast
|
||||||
cast = pychromecast
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# import CEC IGNORE attributes
|
# import CEC IGNORE attributes
|
||||||
ignore_cec = config.get(CONF_IGNORE_CEC, [])
|
ignore_cec = config.get(CONF_IGNORE_CEC, [])
|
||||||
if isinstance(ignore_cec, list):
|
if isinstance(ignore_cec, list):
|
||||||
cast.IGNORE_CEC += ignore_cec
|
pychromecast.IGNORE_CEC += ignore_cec
|
||||||
else:
|
else:
|
||||||
logger.error('Chromecast conig, %s must be a list.', CONF_IGNORE_CEC)
|
logger.error('CEC config "%s" must be a list.', CONF_IGNORE_CEC)
|
||||||
|
|
||||||
hosts = []
|
hosts = []
|
||||||
|
|
||||||
if discovery_info and discovery_info[0] not in KNOWN_HOSTS:
|
if discovery_info and discovery_info in KNOWN_HOSTS:
|
||||||
hosts = [discovery_info[0]]
|
return
|
||||||
|
|
||||||
|
elif discovery_info:
|
||||||
|
hosts = [discovery_info]
|
||||||
|
|
||||||
elif CONF_HOST in config:
|
elif CONF_HOST in config:
|
||||||
hosts = [config[CONF_HOST]]
|
hosts = [(config[CONF_HOST], DEFAULT_PORT)]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
hosts = (host_port[0] for host_port
|
hosts = [tuple(dev[:2]) for dev in pychromecast.discover_chromecasts()
|
||||||
in cast.discover_chromecasts()
|
if tuple(dev[:2]) not in KNOWN_HOSTS]
|
||||||
if host_port[0] not in KNOWN_HOSTS)
|
|
||||||
|
|
||||||
casts = []
|
casts = []
|
||||||
|
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
try:
|
try:
|
||||||
casts.append(CastDevice(host))
|
casts.append(CastDevice(*host))
|
||||||
except cast.ChromecastConnectionError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
KNOWN_HOSTS.append(host)
|
KNOWN_HOSTS.append(host)
|
||||||
|
except pychromecast.ChromecastConnectionError:
|
||||||
|
pass
|
||||||
|
|
||||||
add_devices(casts)
|
add_devices(casts)
|
||||||
|
|
||||||
@ -80,11 +77,9 @@ class CastDevice(MediaPlayerDevice):
|
|||||||
# pylint: disable=abstract-method
|
# pylint: disable=abstract-method
|
||||||
# pylint: disable=too-many-public-methods
|
# pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
def __init__(self, host):
|
def __init__(self, host, port):
|
||||||
import pychromecast.controllers.youtube as youtube
|
import pychromecast
|
||||||
self.cast = cast.Chromecast(host)
|
self.cast = pychromecast.Chromecast(host, port)
|
||||||
self.youtube = youtube.YouTubeController()
|
|
||||||
self.cast.register_handler(self.youtube)
|
|
||||||
|
|
||||||
self.cast.socket_client.receiver_controller.register_status_listener(
|
self.cast.socket_client.receiver_controller.register_status_listener(
|
||||||
self)
|
self)
|
||||||
@ -224,11 +219,13 @@ class CastDevice(MediaPlayerDevice):
|
|||||||
""" Turns on the ChromeCast. """
|
""" Turns on the ChromeCast. """
|
||||||
# The only way we can turn the Chromecast is on is by launching an app
|
# The only way we can turn the Chromecast is on is by launching an app
|
||||||
if not self.cast.status or not self.cast.status.is_active_input:
|
if not self.cast.status or not self.cast.status.is_active_input:
|
||||||
|
import pychromecast
|
||||||
|
|
||||||
if self.cast.app_id:
|
if self.cast.app_id:
|
||||||
self.cast.quit_app()
|
self.cast.quit_app()
|
||||||
|
|
||||||
self.cast.play_media(
|
self.cast.play_media(
|
||||||
CAST_SPLASH, cast.STREAM_TYPE_BUFFERED)
|
CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED)
|
||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
""" Turns Chromecast off. """
|
""" Turns Chromecast off. """
|
||||||
@ -266,10 +263,6 @@ class CastDevice(MediaPlayerDevice):
|
|||||||
""" Plays media from a URL """
|
""" Plays media from a URL """
|
||||||
self.cast.media_controller.play_media(media_id, media_type)
|
self.cast.media_controller.play_media(media_id, media_type)
|
||||||
|
|
||||||
def play_youtube(self, media_id):
|
|
||||||
""" Plays a YouTube media. """
|
|
||||||
self.youtube.play_video(media_id)
|
|
||||||
|
|
||||||
# implementation of chromecast status_listener methods
|
# implementation of chromecast status_listener methods
|
||||||
|
|
||||||
def new_cast_status(self, status):
|
def new_cast_status(self, status):
|
||||||
|
@ -7,11 +7,11 @@ from homeassistant.const import (
|
|||||||
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
|
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
MediaPlayerDevice, YOUTUBE_COVER_URL_FORMAT,
|
MediaPlayerDevice,
|
||||||
MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
|
MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
|
||||||
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_YOUTUBE,
|
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
||||||
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PREVIOUS_TRACK,
|
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PREVIOUS_TRACK,
|
||||||
SUPPORT_NEXT_TRACK)
|
SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
@ -26,9 +26,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg'
|
||||||
|
|
||||||
YOUTUBE_PLAYER_SUPPORT = \
|
YOUTUBE_PLAYER_SUPPORT = \
|
||||||
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||||
SUPPORT_YOUTUBE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA
|
||||||
|
|
||||||
MUSIC_PLAYER_SUPPORT = \
|
MUSIC_PLAYER_SUPPORT = \
|
||||||
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||||
@ -150,10 +152,9 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
|
|||||||
""" Flags of media commands that are supported. """
|
""" Flags of media commands that are supported. """
|
||||||
return YOUTUBE_PLAYER_SUPPORT
|
return YOUTUBE_PLAYER_SUPPORT
|
||||||
|
|
||||||
def play_youtube(self, media_id):
|
def play_media(self, media_type, media_id):
|
||||||
""" Plays a YouTube media. """
|
""" Plays a piece of media. """
|
||||||
self.youtube_id = media_id
|
self.youtube_id = media_id
|
||||||
self._media_title = 'some YouTube video'
|
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
|
||||||
|
|
||||||
@ -234,7 +235,7 @@ class DemoMusicPlayer(AbstractDemoPlayer):
|
|||||||
""" Flags of media commands that are supported. """
|
""" Flags of media commands that are supported. """
|
||||||
support = MUSIC_PLAYER_SUPPORT
|
support = MUSIC_PLAYER_SUPPORT
|
||||||
|
|
||||||
if self._cur_track > 1:
|
if self._cur_track > 0:
|
||||||
support |= SUPPORT_PREVIOUS_TRACK
|
support |= SUPPORT_PREVIOUS_TRACK
|
||||||
|
|
||||||
if self._cur_track < len(self.tracks)-1:
|
if self._cur_track < len(self.tracks)-1:
|
||||||
|
@ -24,7 +24,6 @@ SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
|||||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
""" Sets up the Denon platform. """
|
""" Sets up the Denon platform. """
|
||||||
if not config.get(CONF_HOST):
|
if not config.get(CONF_HOST):
|
||||||
@ -48,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
class DenonDevice(MediaPlayerDevice):
|
class DenonDevice(MediaPlayerDevice):
|
||||||
""" Represents a Denon device. """
|
""" Represents a Denon device. """
|
||||||
|
|
||||||
# pylint: disable=too-many-public-methods
|
# pylint: disable=too-many-public-methods, abstract-method
|
||||||
|
|
||||||
def __init__(self, name, host):
|
def __init__(self, name, host):
|
||||||
self._name = name
|
self._name = name
|
||||||
@ -145,10 +144,6 @@ class DenonDevice(MediaPlayerDevice):
|
|||||||
""" mute (true) or unmute (false) media player. """
|
""" mute (true) or unmute (false) media player. """
|
||||||
self.telnet_command("MU" + ("ON" if mute else "OFF"))
|
self.telnet_command("MU" + ("ON" if mute else "OFF"))
|
||||||
|
|
||||||
def media_play_pause(self):
|
|
||||||
""" media_play_pause media player. """
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def media_play(self):
|
def media_play(self):
|
||||||
""" media_play media player. """
|
""" media_play media player. """
|
||||||
self.telnet_command("NS9A")
|
self.telnet_command("NS9A")
|
||||||
@ -164,9 +159,6 @@ class DenonDevice(MediaPlayerDevice):
|
|||||||
def media_previous_track(self):
|
def media_previous_track(self):
|
||||||
self.telnet_command("NS9E")
|
self.telnet_command("NS9E")
|
||||||
|
|
||||||
def media_seek(self, position):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def turn_on(self):
|
def turn_on(self):
|
||||||
""" turn the media player on. """
|
""" turn the media player on. """
|
||||||
self.telnet_command("PWON")
|
self.telnet_command("PWON")
|
||||||
|
@ -105,6 +105,8 @@ class FireTV(object):
|
|||||||
class FireTVDevice(MediaPlayerDevice):
|
class FireTVDevice(MediaPlayerDevice):
|
||||||
""" Represents an Amazon Fire TV device on the network. """
|
""" Represents an Amazon Fire TV device on the network. """
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method
|
||||||
|
|
||||||
def __init__(self, host, device, name):
|
def __init__(self, host, device, name):
|
||||||
self._firetv = FireTV(host, device)
|
self._firetv = FireTV(host, device)
|
||||||
self._name = name
|
self._name = name
|
||||||
@ -176,15 +178,3 @@ class FireTVDevice(MediaPlayerDevice):
|
|||||||
def media_next_track(self):
|
def media_next_track(self):
|
||||||
""" Send next track command (results in fast-forward). """
|
""" Send next track command (results in fast-forward). """
|
||||||
self._firetv.action('media_next')
|
self._firetv.action('media_next')
|
||||||
|
|
||||||
def media_seek(self, position):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def mute_volume(self, mute):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def play_youtube(self, media_id):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def set_volume_level(self, volume):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
@ -22,43 +22,41 @@ SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
|||||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK
|
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
""" Sets up the kodi platform. """
|
""" Sets up the kodi platform. """
|
||||||
|
|
||||||
|
url = '{}:{}'.format(config.get('host'), config.get('port', '8080'))
|
||||||
|
|
||||||
|
jsonrpc_url = config.get('url') # deprecated
|
||||||
|
if jsonrpc_url:
|
||||||
|
url = jsonrpc_url.rstrip('/jsonrpc')
|
||||||
|
|
||||||
add_devices([
|
add_devices([
|
||||||
KodiDevice(
|
KodiDevice(
|
||||||
config.get('name', 'Kodi'),
|
config.get('name', 'Kodi'),
|
||||||
config.get('url'),
|
url,
|
||||||
auth=(
|
auth=(
|
||||||
config.get('user', ''),
|
config.get('user', ''),
|
||||||
config.get('password', ''))),
|
config.get('password', ''))),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
def _get_image_url(kodi_url):
|
|
||||||
""" Helper function that parses the thumbnail URLs used by Kodi. """
|
|
||||||
url_components = urllib.parse.urlparse(kodi_url)
|
|
||||||
|
|
||||||
if url_components.scheme == 'image':
|
|
||||||
return urllib.parse.unquote(url_components.netloc)
|
|
||||||
|
|
||||||
|
|
||||||
class KodiDevice(MediaPlayerDevice):
|
class KodiDevice(MediaPlayerDevice):
|
||||||
""" Represents a XBMC/Kodi device. """
|
""" Represents a XBMC/Kodi device. """
|
||||||
|
|
||||||
# pylint: disable=too-many-public-methods
|
# pylint: disable=too-many-public-methods, abstract-method
|
||||||
|
|
||||||
def __init__(self, name, url, auth=None):
|
def __init__(self, name, url, auth=None):
|
||||||
import jsonrpc_requests
|
import jsonrpc_requests
|
||||||
self._name = name
|
self._name = name
|
||||||
self._url = url
|
self._url = url
|
||||||
self._server = jsonrpc_requests.Server(url, auth=auth)
|
self._server = jsonrpc_requests.Server(
|
||||||
|
'{}/jsonrpc'.format(self._url),
|
||||||
|
auth=auth)
|
||||||
self._players = None
|
self._players = None
|
||||||
self._properties = None
|
self._properties = None
|
||||||
self._item = None
|
self._item = None
|
||||||
self._app_properties = None
|
self._app_properties = None
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -156,7 +154,16 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
def media_image_url(self):
|
def media_image_url(self):
|
||||||
""" Image url of current playing media. """
|
""" Image url of current playing media. """
|
||||||
if self._item is not None:
|
if self._item is not None:
|
||||||
return _get_image_url(self._item['thumbnail'])
|
return self._get_image_url()
|
||||||
|
|
||||||
|
def _get_image_url(self):
|
||||||
|
""" Helper function that parses the thumbnail URLs used by Kodi. """
|
||||||
|
url_components = urllib.parse.urlparse(self._item['thumbnail'])
|
||||||
|
|
||||||
|
if url_components.scheme == 'image':
|
||||||
|
return '{}/image/{}'.format(
|
||||||
|
self._url,
|
||||||
|
urllib.parse.quote_plus(self._item['thumbnail']))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_title(self):
|
def media_title(self):
|
||||||
@ -263,11 +270,3 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
self._server.Player.Seek(players[0]['playerid'], time)
|
self._server.Player.Seek(players[0]['playerid'], time)
|
||||||
|
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
|
||||||
def turn_on(self):
|
|
||||||
""" turn the media player on. """
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def play_youtube(self, media_id):
|
|
||||||
""" Plays a YouTube media. """
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
@ -59,7 +59,7 @@ def config_from_file(filename, config=None):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method, unused-argument
|
# pylint: disable=abstract-method
|
||||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
""" Sets up the plex platform. """
|
""" Sets up the plex platform. """
|
||||||
|
|
||||||
|
170
homeassistant/components/media_player/samsungtv.py
Normal file
170
homeassistant/components/media_player/samsungtv.py
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.media_player.denon
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Provides an interface to Samsung TV with a Laninterface.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/media_player.samsungtv/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from homeassistant.components.media_player import (
|
||||||
|
MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_VOLUME_STEP,
|
||||||
|
SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK,
|
||||||
|
SUPPORT_NEXT_TRACK, SUPPORT_TURN_OFF,
|
||||||
|
DOMAIN)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST, CONF_NAME, STATE_OFF,
|
||||||
|
STATE_ON, STATE_UNKNOWN)
|
||||||
|
|
||||||
|
from homeassistant.helpers import validate_config
|
||||||
|
|
||||||
|
CONF_PORT = "port"
|
||||||
|
CONF_TIMEOUT = "timeout"
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
REQUIREMENTS = ['samsungctl==0.5.1']
|
||||||
|
|
||||||
|
SUPPORT_SAMSUNGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
||||||
|
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
|
||||||
|
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Sets up the Samsung TV platform. """
|
||||||
|
|
||||||
|
# Validate that all required config options are given
|
||||||
|
if not validate_config({DOMAIN: config}, {DOMAIN: [CONF_HOST]}, _LOGGER):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Default the entity_name to 'Samsung TV Remote'
|
||||||
|
name = config.get(CONF_NAME, 'Samsung TV Remote')
|
||||||
|
|
||||||
|
# Generate a config for the Samsung lib
|
||||||
|
remote_config = {
|
||||||
|
"name": "HomeAssistant",
|
||||||
|
"description": config.get(CONF_NAME, ''),
|
||||||
|
"id": "ha.component.samsung",
|
||||||
|
"port": config.get(CONF_PORT, 55000),
|
||||||
|
"host": config.get(CONF_HOST),
|
||||||
|
"timeout": config.get(CONF_TIMEOUT, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
add_devices([SamsungTVDevice(name, remote_config)])
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method
|
||||||
|
class SamsungTVDevice(MediaPlayerDevice):
|
||||||
|
""" Represents a Samsung TV. """
|
||||||
|
|
||||||
|
# pylint: disable=too-many-public-methods
|
||||||
|
def __init__(self, name, config):
|
||||||
|
from samsungctl import Remote
|
||||||
|
# Save a reference to the imported class
|
||||||
|
self._remote_class = Remote
|
||||||
|
self._name = name
|
||||||
|
# Assume that the TV is not muted
|
||||||
|
self._muted = False
|
||||||
|
# Assume that the TV is in Play mode
|
||||||
|
self._playing = True
|
||||||
|
self._state = STATE_UNKNOWN
|
||||||
|
self._remote = None
|
||||||
|
self._config = config
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
# Send an empty key to see if we are still connected
|
||||||
|
return self.send_key('KEY_POWER')
|
||||||
|
|
||||||
|
def get_remote(self):
|
||||||
|
""" Creates or Returns a remote control instance """
|
||||||
|
|
||||||
|
if self._remote is None:
|
||||||
|
# We need to create a new instance to reconnect.
|
||||||
|
self._remote = self._remote_class(self._config)
|
||||||
|
|
||||||
|
return self._remote
|
||||||
|
|
||||||
|
def send_key(self, key):
|
||||||
|
""" Sends a key to the tv and handles exceptions """
|
||||||
|
try:
|
||||||
|
self.get_remote().control(key)
|
||||||
|
self._state = STATE_ON
|
||||||
|
except (self._remote_class.UnhandledResponse,
|
||||||
|
self._remote_class.AccessDenied, BrokenPipeError):
|
||||||
|
# We got a response so it's on.
|
||||||
|
# BrokenPipe can occur when the commands is sent to fast
|
||||||
|
self._state = STATE_ON
|
||||||
|
self._remote = None
|
||||||
|
return False
|
||||||
|
except (self._remote_class.ConnectionClosed, socket.timeout,
|
||||||
|
TimeoutError, OSError):
|
||||||
|
self._state = STATE_OFF
|
||||||
|
self._remote = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Returns the name of the device. """
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_volume_muted(self):
|
||||||
|
""" Boolean if volume is currently muted. """
|
||||||
|
return self._muted
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_media_commands(self):
|
||||||
|
""" Flags of media commands that are supported. """
|
||||||
|
return SUPPORT_SAMSUNGTV
|
||||||
|
|
||||||
|
def turn_off(self):
|
||||||
|
""" turn_off media player. """
|
||||||
|
self.send_key("KEY_POWEROFF")
|
||||||
|
|
||||||
|
def volume_up(self):
|
||||||
|
""" volume_up media player. """
|
||||||
|
self.send_key("KEY_VOLUP")
|
||||||
|
|
||||||
|
def volume_down(self):
|
||||||
|
""" volume_down media player. """
|
||||||
|
self.send_key("KEY_VOLDOWN")
|
||||||
|
|
||||||
|
def mute_volume(self, mute):
|
||||||
|
self.send_key("KEY_MUTE")
|
||||||
|
|
||||||
|
def media_play_pause(self):
|
||||||
|
""" Simulate play pause media player. """
|
||||||
|
if self._playing:
|
||||||
|
self.media_pause()
|
||||||
|
else:
|
||||||
|
self.media_play()
|
||||||
|
|
||||||
|
def media_play(self):
|
||||||
|
""" media_play media player. """
|
||||||
|
self._playing = True
|
||||||
|
self.send_key("KEY_PLAY")
|
||||||
|
|
||||||
|
def media_pause(self):
|
||||||
|
""" media_pause media player. """
|
||||||
|
self._playing = False
|
||||||
|
self.send_key("KEY_PAUSE")
|
||||||
|
|
||||||
|
def media_next_track(self):
|
||||||
|
""" Send next track command. """
|
||||||
|
self.send_key("KEY_FF")
|
||||||
|
|
||||||
|
def media_previous_track(self):
|
||||||
|
self.send_key("KEY_REWIND")
|
||||||
|
|
||||||
|
def turn_on(self):
|
||||||
|
""" turn the media player on. """
|
||||||
|
self.send_key("KEY_POWERON")
|
85
homeassistant/components/media_player/snapcast.py
Normal file
85
homeassistant/components/media_player/snapcast.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.media_player.snapcast
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Provides functionality to interact with Snapcast clients.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/media_player.snapcast/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
STATE_ON, STATE_OFF)
|
||||||
|
|
||||||
|
from homeassistant.components.media_player import (
|
||||||
|
MediaPlayerDevice,
|
||||||
|
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE)
|
||||||
|
|
||||||
|
SUPPORT_SNAPCAST = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
|
||||||
|
DOMAIN = 'snapcast'
|
||||||
|
REQUIREMENTS = ['snapcast==1.1.1']
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Sets up the Snapcast platform. """
|
||||||
|
import snapcast.control
|
||||||
|
host = config.get('host')
|
||||||
|
port = config.get('port', snapcast.control.CONTROL_PORT)
|
||||||
|
if not host:
|
||||||
|
_LOGGER.error('No snapserver host specified')
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
server = snapcast.control.Snapserver(host, port)
|
||||||
|
except socket.gaierror:
|
||||||
|
_LOGGER.error('Could not connect to Snapcast server at %s:%d',
|
||||||
|
host, port)
|
||||||
|
return
|
||||||
|
add_devices([SnapcastDevice(client) for client in server.clients])
|
||||||
|
|
||||||
|
|
||||||
|
class SnapcastDevice(MediaPlayerDevice):
|
||||||
|
""" Represents a Snapcast client device. """
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method
|
||||||
|
|
||||||
|
def __init__(self, client):
|
||||||
|
self._client = client
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Device name. """
|
||||||
|
return self._client.identifier
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume_level(self):
|
||||||
|
""" Volume level. """
|
||||||
|
return self._client.volume / 100
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_volume_muted(self):
|
||||||
|
""" Volume muted. """
|
||||||
|
return self._client.muted
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_media_commands(self):
|
||||||
|
""" Flags of media commands that are supported. """
|
||||||
|
return SUPPORT_SNAPCAST
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" State of the player. """
|
||||||
|
if self._client.connected:
|
||||||
|
return STATE_ON
|
||||||
|
return STATE_OFF
|
||||||
|
|
||||||
|
def mute_volume(self, mute):
|
||||||
|
""" Mute status. """
|
||||||
|
self._client.muted = mute
|
||||||
|
|
||||||
|
def set_volume_level(self, volume):
|
||||||
|
""" Volume level. """
|
||||||
|
self._client.volume = round(volume * 100)
|
@ -27,7 +27,6 @@ SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \
|
|||||||
SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
""" Sets up the squeezebox platform. """
|
""" Sets up the squeezebox platform. """
|
||||||
if not config.get(CONF_HOST):
|
if not config.get(CONF_HOST):
|
||||||
@ -138,7 +137,7 @@ class LogitechMediaServer(object):
|
|||||||
class SqueezeBoxDevice(MediaPlayerDevice):
|
class SqueezeBoxDevice(MediaPlayerDevice):
|
||||||
""" Represents a SqueezeBox device. """
|
""" Represents a SqueezeBox device. """
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments, abstract-method
|
||||||
def __init__(self, lms, player_id):
|
def __init__(self, lms, player_id):
|
||||||
super(SqueezeBoxDevice, self).__init__()
|
super(SqueezeBoxDevice, self).__init__()
|
||||||
self._lms = lms
|
self._lms = lms
|
||||||
@ -292,7 +291,3 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
|||||||
""" turn the media player on. """
|
""" turn the media player on. """
|
||||||
self._lms.query(self._id, 'power', '1')
|
self._lms.query(self._id, 'power', '1')
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
|
||||||
def play_youtube(self, media_id):
|
|
||||||
""" Plays a YouTube media. """
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
@ -27,7 +27,7 @@ from homeassistant.components.media_player import (
|
|||||||
MediaPlayerDevice, DOMAIN,
|
MediaPlayerDevice, DOMAIN,
|
||||||
SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
||||||
SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
|
SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
|
||||||
SERVICE_PLAY_MEDIA, SERVICE_YOUTUBE_VIDEO,
|
SERVICE_PLAY_MEDIA,
|
||||||
ATTR_SUPPORTED_MEDIA_COMMANDS, ATTR_MEDIA_VOLUME_MUTED,
|
ATTR_SUPPORTED_MEDIA_COMMANDS, ATTR_MEDIA_VOLUME_MUTED,
|
||||||
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION,
|
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION,
|
||||||
ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST, ATTR_MEDIA_ALBUM_NAME,
|
ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST, ATTR_MEDIA_ALBUM_NAME,
|
||||||
@ -215,15 +215,6 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _cache_active_child_state(self):
|
|
||||||
""" The state of the active child or None """
|
|
||||||
for child_name in self._children:
|
|
||||||
child_state = self.hass.states.get(child_name)
|
|
||||||
if child_state and child_state.state not in OFF_STATES:
|
|
||||||
self._child_state = child_state
|
|
||||||
return
|
|
||||||
self._child_state = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
""" name of universal player """
|
""" name of universal player """
|
||||||
@ -406,11 +397,6 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
|||||||
data = {ATTR_MEDIA_SEEK_POSITION: position}
|
data = {ATTR_MEDIA_SEEK_POSITION: position}
|
||||||
self._call_service(SERVICE_MEDIA_SEEK, data)
|
self._call_service(SERVICE_MEDIA_SEEK, data)
|
||||||
|
|
||||||
def play_youtube(self, media_id):
|
|
||||||
""" Plays a YouTube media. """
|
|
||||||
data = {'media_id': media_id}
|
|
||||||
self._call_service(SERVICE_YOUTUBE_VIDEO, data)
|
|
||||||
|
|
||||||
def play_media(self, media_type, media_id):
|
def play_media(self, media_type, media_id):
|
||||||
""" Plays a piece of media. """
|
""" Plays a piece of media. """
|
||||||
data = {'media_type': media_type, 'media_id': media_id}
|
data = {'media_type': media_type, 'media_id': media_id}
|
||||||
|
@ -12,8 +12,10 @@ import socket
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
from homeassistant.config import load_yaml_config_file
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
|
from homeassistant.util import template
|
||||||
from homeassistant.helpers import validate_config
|
from homeassistant.helpers import validate_config
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||||
@ -49,24 +51,34 @@ DEFAULT_PROTOCOL = PROTOCOL_311
|
|||||||
|
|
||||||
ATTR_TOPIC = 'topic'
|
ATTR_TOPIC = 'topic'
|
||||||
ATTR_PAYLOAD = 'payload'
|
ATTR_PAYLOAD = 'payload'
|
||||||
|
ATTR_PAYLOAD_TEMPLATE = 'payload_template'
|
||||||
ATTR_QOS = 'qos'
|
ATTR_QOS = 'qos'
|
||||||
ATTR_RETAIN = 'retain'
|
ATTR_RETAIN = 'retain'
|
||||||
|
|
||||||
MAX_RECONNECT_WAIT = 300 # seconds
|
MAX_RECONNECT_WAIT = 300 # seconds
|
||||||
|
|
||||||
|
|
||||||
def publish(hass, topic, payload, qos=None, retain=None):
|
def _build_publish_data(topic, qos, retain):
|
||||||
"""Publish message to an MQTT topic."""
|
"""Build the arguments for the publish service without the payload."""
|
||||||
data = {
|
data = {ATTR_TOPIC: topic}
|
||||||
ATTR_TOPIC: topic,
|
|
||||||
ATTR_PAYLOAD: payload,
|
|
||||||
}
|
|
||||||
if qos is not None:
|
if qos is not None:
|
||||||
data[ATTR_QOS] = qos
|
data[ATTR_QOS] = qos
|
||||||
|
|
||||||
if retain is not None:
|
if retain is not None:
|
||||||
data[ATTR_RETAIN] = retain
|
data[ATTR_RETAIN] = retain
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def publish(hass, topic, payload, qos=None, retain=None):
|
||||||
|
"""Publish message to an MQTT topic."""
|
||||||
|
data = _build_publish_data(topic, qos, retain)
|
||||||
|
data[ATTR_PAYLOAD] = payload
|
||||||
|
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
|
||||||
|
|
||||||
|
|
||||||
|
def publish_template(hass, topic, payload_template, qos=None, retain=None):
|
||||||
|
"""Publish message to an MQTT topic using a template payload."""
|
||||||
|
data = _build_publish_data(topic, qos, retain)
|
||||||
|
data[ATTR_PAYLOAD_TEMPLATE] = payload_template
|
||||||
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
|
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
|
||||||
|
|
||||||
|
|
||||||
@ -132,15 +144,34 @@ def setup(hass, config):
|
|||||||
"""Handle MQTT publish service calls."""
|
"""Handle MQTT publish service calls."""
|
||||||
msg_topic = call.data.get(ATTR_TOPIC)
|
msg_topic = call.data.get(ATTR_TOPIC)
|
||||||
payload = call.data.get(ATTR_PAYLOAD)
|
payload = call.data.get(ATTR_PAYLOAD)
|
||||||
|
payload_template = call.data.get(ATTR_PAYLOAD_TEMPLATE)
|
||||||
qos = call.data.get(ATTR_QOS, DEFAULT_QOS)
|
qos = call.data.get(ATTR_QOS, DEFAULT_QOS)
|
||||||
retain = call.data.get(ATTR_RETAIN, DEFAULT_RETAIN)
|
retain = call.data.get(ATTR_RETAIN, DEFAULT_RETAIN)
|
||||||
|
if payload is None:
|
||||||
|
if payload_template is None:
|
||||||
|
_LOGGER.error(
|
||||||
|
"You must set either '%s' or '%s' to use this service",
|
||||||
|
ATTR_PAYLOAD, ATTR_PAYLOAD_TEMPLATE)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
payload = template.render(hass, payload_template)
|
||||||
|
except template.jinja2.TemplateError as exc:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Unable to publish to '%s': rendering payload template of "
|
||||||
|
"'%s' failed because %s.",
|
||||||
|
msg_topic, payload_template, exc)
|
||||||
|
return
|
||||||
if msg_topic is None or payload is None:
|
if msg_topic is None or payload is None:
|
||||||
return
|
return
|
||||||
MQTT_CLIENT.publish(msg_topic, payload, qos, retain)
|
MQTT_CLIENT.publish(msg_topic, payload, qos, retain)
|
||||||
|
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_mqtt)
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_mqtt)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_PUBLISH, publish_service)
|
descriptions = load_yaml_config_file(
|
||||||
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
|
hass.services.register(DOMAIN, SERVICE_PUBLISH, publish_service,
|
||||||
|
descriptions.get(SERVICE_PUBLISH))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
29
homeassistant/components/mqtt/services.yaml
Normal file
29
homeassistant/components/mqtt/services.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
publish:
|
||||||
|
description: Publish a message to an MQTT topic
|
||||||
|
|
||||||
|
fields:
|
||||||
|
topic:
|
||||||
|
description: Topic to publish payload
|
||||||
|
example: /homeassistant/hello
|
||||||
|
|
||||||
|
payload:
|
||||||
|
description: Payload to publish
|
||||||
|
example: This is great
|
||||||
|
|
||||||
|
payload_template:
|
||||||
|
description: Template to render as payload value. Ignored if payload given.
|
||||||
|
example: "{{ states('sensor.temperature') }}"
|
||||||
|
|
||||||
|
qos:
|
||||||
|
description: Quality of Service
|
||||||
|
example: 2
|
||||||
|
values:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
default: 0
|
||||||
|
|
||||||
|
retain:
|
||||||
|
description: If message should have the retain flag set.
|
||||||
|
example: true
|
||||||
|
default: false
|
@ -11,6 +11,7 @@ from homeassistant.core import EventOrigin, State
|
|||||||
from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN
|
from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN
|
||||||
from homeassistant.components.mqtt import SERVICE_PUBLISH as MQTT_SVC_PUBLISH
|
from homeassistant.components.mqtt import SERVICE_PUBLISH as MQTT_SVC_PUBLISH
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_SERVICE_DATA,
|
||||||
MATCH_ALL,
|
MATCH_ALL,
|
||||||
EVENT_TIME_CHANGED,
|
EVENT_TIME_CHANGED,
|
||||||
EVENT_CALL_SERVICE,
|
EVENT_CALL_SERVICE,
|
||||||
@ -46,7 +47,7 @@ def setup(hass, config):
|
|||||||
if (
|
if (
|
||||||
event.data.get('domain') == MQTT_DOMAIN and
|
event.data.get('domain') == MQTT_DOMAIN and
|
||||||
event.data.get('service') == MQTT_SVC_PUBLISH and
|
event.data.get('service') == MQTT_SVC_PUBLISH and
|
||||||
event.data.get('topic') == pub_topic
|
event.data[ATTR_SERVICE_DATA].get('topic') == pub_topic
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
homeassistant.components.mysensors
|
homeassistant.components.mysensors.
|
||||||
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
MySensors component that connects to a MySensors gateway via pymysensors
|
MySensors component that connects to a MySensors gateway via pymysensors
|
||||||
API.
|
API.
|
||||||
@ -24,13 +25,16 @@ CONF_DEBUG = 'debug'
|
|||||||
CONF_PERSISTENCE = 'persistence'
|
CONF_PERSISTENCE = 'persistence'
|
||||||
CONF_PERSISTENCE_FILE = 'persistence_file'
|
CONF_PERSISTENCE_FILE = 'persistence_file'
|
||||||
CONF_VERSION = 'version'
|
CONF_VERSION = 'version'
|
||||||
|
CONF_BAUD_RATE = 'baud_rate'
|
||||||
|
CONF_OPTIMISTIC = 'optimistic'
|
||||||
DEFAULT_VERSION = '1.4'
|
DEFAULT_VERSION = '1.4'
|
||||||
|
DEFAULT_BAUD_RATE = 115200
|
||||||
|
|
||||||
DOMAIN = 'mysensors'
|
DOMAIN = 'mysensors'
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
REQUIREMENTS = [
|
REQUIREMENTS = [
|
||||||
'https://github.com/theolind/pymysensors/archive/'
|
'https://github.com/theolind/pymysensors/archive/'
|
||||||
'005bff4c5ca7a56acd30e816bc3bcdb5cb2d46fd.zip#pymysensors==0.4']
|
'f0c928532167fb24823efa793ec21ca646fd37a6.zip#pymysensors==0.5']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
ATTR_NODE_ID = 'node_id'
|
ATTR_NODE_ID = 'node_id'
|
||||||
ATTR_CHILD_ID = 'child_id'
|
ATTR_CHILD_ID = 'child_id'
|
||||||
@ -38,13 +42,15 @@ ATTR_PORT = 'port'
|
|||||||
|
|
||||||
GATEWAYS = None
|
GATEWAYS = None
|
||||||
|
|
||||||
DISCOVER_SENSORS = "mysensors.sensors"
|
DISCOVER_SENSORS = 'mysensors.sensors'
|
||||||
DISCOVER_SWITCHES = "mysensors.switches"
|
DISCOVER_SWITCHES = 'mysensors.switches'
|
||||||
|
DISCOVER_LIGHTS = 'mysensors.lights'
|
||||||
|
|
||||||
# Maps discovered services to their platforms
|
# Maps discovered services to their platforms
|
||||||
DISCOVERY_COMPONENTS = [
|
DISCOVERY_COMPONENTS = [
|
||||||
('sensor', DISCOVER_SENSORS),
|
('sensor', DISCOVER_SENSORS),
|
||||||
('switch', DISCOVER_SWITCHES),
|
('switch', DISCOVER_SWITCHES),
|
||||||
|
('light', DISCOVER_LIGHTS),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -54,21 +60,28 @@ def setup(hass, config):
|
|||||||
{DOMAIN: [CONF_GATEWAYS]},
|
{DOMAIN: [CONF_GATEWAYS]},
|
||||||
_LOGGER):
|
_LOGGER):
|
||||||
return False
|
return False
|
||||||
|
if not all(CONF_PORT in gateway
|
||||||
|
for gateway in config[DOMAIN][CONF_GATEWAYS]):
|
||||||
|
_LOGGER.error('Missing required configuration items '
|
||||||
|
'in %s: %s', DOMAIN, CONF_PORT)
|
||||||
|
return False
|
||||||
|
|
||||||
import mysensors.mysensors as mysensors
|
import mysensors.mysensors as mysensors
|
||||||
|
|
||||||
version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION))
|
version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION))
|
||||||
is_metric = (hass.config.temperature_unit == TEMP_CELCIUS)
|
is_metric = (hass.config.temperature_unit == TEMP_CELCIUS)
|
||||||
|
|
||||||
def setup_gateway(port, persistence, persistence_file, version):
|
def setup_gateway(port, persistence, persistence_file, version, baud_rate):
|
||||||
"""Return gateway after setup of the gateway."""
|
"""Return gateway after setup of the gateway."""
|
||||||
gateway = mysensors.SerialGateway(port, event_callback=None,
|
gateway = mysensors.SerialGateway(port, event_callback=None,
|
||||||
persistence=persistence,
|
persistence=persistence,
|
||||||
persistence_file=persistence_file,
|
persistence_file=persistence_file,
|
||||||
protocol_version=version)
|
protocol_version=version,
|
||||||
|
baud=baud_rate)
|
||||||
gateway.metric = is_metric
|
gateway.metric = is_metric
|
||||||
gateway.debug = config[DOMAIN].get(CONF_DEBUG, False)
|
gateway.debug = config[DOMAIN].get(CONF_DEBUG, False)
|
||||||
gateway = GatewayWrapper(gateway, version)
|
optimistic = config[DOMAIN].get(CONF_OPTIMISTIC, False)
|
||||||
|
gateway = GatewayWrapper(gateway, version, optimistic)
|
||||||
# pylint: disable=attribute-defined-outside-init
|
# pylint: disable=attribute-defined-outside-init
|
||||||
gateway.event_callback = gateway.callback_factory()
|
gateway.event_callback = gateway.callback_factory()
|
||||||
|
|
||||||
@ -98,8 +111,9 @@ def setup(hass, config):
|
|||||||
persistence_file = gway.get(
|
persistence_file = gway.get(
|
||||||
CONF_PERSISTENCE_FILE,
|
CONF_PERSISTENCE_FILE,
|
||||||
hass.config.path('mysensors{}.pickle'.format(index + 1)))
|
hass.config.path('mysensors{}.pickle'.format(index + 1)))
|
||||||
|
baud_rate = gway.get(CONF_BAUD_RATE, DEFAULT_BAUD_RATE)
|
||||||
GATEWAYS[port] = setup_gateway(
|
GATEWAYS[port] = setup_gateway(
|
||||||
port, persistence, persistence_file, version)
|
port, persistence, persistence_file, version, baud_rate)
|
||||||
|
|
||||||
for (component, discovery_service) in DISCOVERY_COMPONENTS:
|
for (component, discovery_service) in DISCOVERY_COMPONENTS:
|
||||||
# Ensure component is loaded
|
# Ensure component is loaded
|
||||||
@ -145,12 +159,13 @@ def pf_callback_factory(map_sv_types, devices, add_devices, entity_class):
|
|||||||
class GatewayWrapper(object):
|
class GatewayWrapper(object):
|
||||||
"""Gateway wrapper class, by subclassing serial gateway."""
|
"""Gateway wrapper class, by subclassing serial gateway."""
|
||||||
|
|
||||||
def __init__(self, gateway, version):
|
def __init__(self, gateway, version, optimistic):
|
||||||
"""Setup class attributes on instantiation.
|
"""Setup class attributes on instantiation.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
gateway (mysensors.SerialGateway): Gateway to wrap.
|
gateway (mysensors.SerialGateway): Gateway to wrap.
|
||||||
version (str): Version of mysensors API.
|
version (str): Version of mysensors API.
|
||||||
|
optimistic (bool): Send values to actuators without feedback state.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
_wrapped_gateway (mysensors.SerialGateway): Wrapped gateway.
|
_wrapped_gateway (mysensors.SerialGateway): Wrapped gateway.
|
||||||
@ -163,6 +178,7 @@ class GatewayWrapper(object):
|
|||||||
self.version = version
|
self.version = version
|
||||||
self.platform_callbacks = []
|
self.platform_callbacks = []
|
||||||
self.const = self.get_const()
|
self.const = self.get_const()
|
||||||
|
self.optimistic = optimistic
|
||||||
self.__initialised = True
|
self.__initialised = True
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
@ -195,7 +211,7 @@ class GatewayWrapper(object):
|
|||||||
"""Return a new callback function."""
|
"""Return a new callback function."""
|
||||||
def node_update(update_type, node_id):
|
def node_update(update_type, node_id):
|
||||||
"""Callback for node updates from the MySensors gateway."""
|
"""Callback for node updates from the MySensors gateway."""
|
||||||
_LOGGER.info('update %s: node %s', update_type, node_id)
|
_LOGGER.debug('update %s: node %s', update_type, node_id)
|
||||||
for callback in self.platform_callbacks:
|
for callback in self.platform_callbacks:
|
||||||
callback(self, node_id)
|
callback(self, node_id)
|
||||||
|
|
||||||
|
80
homeassistant/components/notify/rest.py
Normal file
80
homeassistant/components/notify/rest.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.notify.rest
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
REST platform for notify component.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/notify.rest/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from homeassistant.helpers import validate_config
|
||||||
|
from homeassistant.components.notify import (
|
||||||
|
DOMAIN, ATTR_TITLE, ATTR_TARGET, BaseNotificationService)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_METHOD = 'GET'
|
||||||
|
DEFAULT_MESSAGE_PARAM_NAME = 'message'
|
||||||
|
DEFAULT_TITLE_PARAM_NAME = None
|
||||||
|
DEFAULT_TARGET_PARAM_NAME = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_service(hass, config):
|
||||||
|
""" Get the REST notification service. """
|
||||||
|
|
||||||
|
if not validate_config({DOMAIN: config},
|
||||||
|
{DOMAIN: ['resource', ]},
|
||||||
|
_LOGGER):
|
||||||
|
return None
|
||||||
|
|
||||||
|
method = config.get('method', DEFAULT_METHOD)
|
||||||
|
message_param_name = config.get('message_param_name',
|
||||||
|
DEFAULT_MESSAGE_PARAM_NAME)
|
||||||
|
title_param_name = config.get('title_param_name',
|
||||||
|
DEFAULT_TITLE_PARAM_NAME)
|
||||||
|
target_param_name = config.get('target_param_name',
|
||||||
|
DEFAULT_TARGET_PARAM_NAME)
|
||||||
|
|
||||||
|
return RestNotificationService(config['resource'], method,
|
||||||
|
message_param_name, title_param_name,
|
||||||
|
target_param_name)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-few-public-methods, too-many-arguments
|
||||||
|
class RestNotificationService(BaseNotificationService):
|
||||||
|
""" Implements notification service for REST. """
|
||||||
|
|
||||||
|
def __init__(self, resource, method, message_param_name,
|
||||||
|
title_param_name, target_param_name):
|
||||||
|
self._resource = resource
|
||||||
|
self._method = method.upper()
|
||||||
|
self._message_param_name = message_param_name
|
||||||
|
self._title_param_name = title_param_name
|
||||||
|
self._target_param_name = target_param_name
|
||||||
|
|
||||||
|
def send_message(self, message="", **kwargs):
|
||||||
|
""" Send a message to a user. """
|
||||||
|
|
||||||
|
data = {
|
||||||
|
self._message_param_name: message
|
||||||
|
}
|
||||||
|
|
||||||
|
if self._title_param_name is not None:
|
||||||
|
data[self._title_param_name] = kwargs.get(ATTR_TITLE)
|
||||||
|
|
||||||
|
if self._target_param_name is not None:
|
||||||
|
data[self._title_param_name] = kwargs.get(ATTR_TARGET)
|
||||||
|
|
||||||
|
if self._method == 'POST':
|
||||||
|
response = requests.post(self._resource, data=data, timeout=10)
|
||||||
|
elif self._method == 'POST_JSON':
|
||||||
|
response = requests.post(self._resource, json=data, timeout=10)
|
||||||
|
else: # default GET
|
||||||
|
response = requests.get(self._resource, params=data, timeout=10)
|
||||||
|
|
||||||
|
if response.status_code not in (200, 201):
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Error sending message. Response %d: %s:",
|
||||||
|
response.status_code, response.reason)
|
238
homeassistant/components/proximity.py
Normal file
238
homeassistant/components/proximity.py
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.proximity
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Component to monitor the proximity of devices to a particular zone and the
|
||||||
|
direction of travel.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/proximity/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from homeassistant.helpers.event import track_state_change
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.util.location import distance
|
||||||
|
|
||||||
|
DEPENDENCIES = ['zone', 'device_tracker']
|
||||||
|
|
||||||
|
DOMAIN = 'proximity'
|
||||||
|
|
||||||
|
# Default tolerance
|
||||||
|
DEFAULT_TOLERANCE = 1
|
||||||
|
|
||||||
|
# Default zone
|
||||||
|
DEFAULT_PROXIMITY_ZONE = 'home'
|
||||||
|
|
||||||
|
# Entity attributes
|
||||||
|
ATTR_DIST_FROM = 'dist_to_zone'
|
||||||
|
ATTR_DIR_OF_TRAVEL = 'dir_of_travel'
|
||||||
|
ATTR_NEAREST = 'nearest'
|
||||||
|
ATTR_FRIENDLY_NAME = 'friendly_name'
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config): # pylint: disable=too-many-locals,too-many-statements
|
||||||
|
""" Get the zones and offsets from configuration.yaml. """
|
||||||
|
ignored_zones = []
|
||||||
|
if 'ignored_zones' in config[DOMAIN]:
|
||||||
|
for variable in config[DOMAIN]['ignored_zones']:
|
||||||
|
ignored_zones.append(variable)
|
||||||
|
|
||||||
|
# Get the devices from configuration.yaml
|
||||||
|
if 'devices' not in config[DOMAIN]:
|
||||||
|
_LOGGER.error('devices not found in config')
|
||||||
|
return False
|
||||||
|
|
||||||
|
proximity_devices = []
|
||||||
|
for variable in config[DOMAIN]['devices']:
|
||||||
|
proximity_devices.append(variable)
|
||||||
|
|
||||||
|
# Get the direction of travel tolerance from configuration.yaml
|
||||||
|
tolerance = config[DOMAIN].get('tolerance', DEFAULT_TOLERANCE)
|
||||||
|
|
||||||
|
# Get the zone to monitor proximity to from configuration.yaml
|
||||||
|
proximity_zone = config[DOMAIN].get('zone', DEFAULT_PROXIMITY_ZONE)
|
||||||
|
|
||||||
|
entity_id = DOMAIN + '.' + proximity_zone
|
||||||
|
proximity_zone = 'zone.' + proximity_zone
|
||||||
|
|
||||||
|
state = hass.states.get(proximity_zone)
|
||||||
|
zone_friendly_name = (state.name).lower()
|
||||||
|
|
||||||
|
# set the default values
|
||||||
|
dist_to_zone = 'not set'
|
||||||
|
dir_of_travel = 'not set'
|
||||||
|
nearest = 'not set'
|
||||||
|
|
||||||
|
proximity = Proximity(hass, zone_friendly_name, dist_to_zone,
|
||||||
|
dir_of_travel, nearest, ignored_zones,
|
||||||
|
proximity_devices, tolerance, proximity_zone)
|
||||||
|
proximity.entity_id = entity_id
|
||||||
|
|
||||||
|
proximity.update_ha_state()
|
||||||
|
|
||||||
|
# Main command to monitor proximity of devices
|
||||||
|
track_state_change(hass, proximity_devices,
|
||||||
|
proximity.check_proximity_state_change)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Proximity(Entity): # pylint: disable=too-many-instance-attributes
|
||||||
|
""" Represents a Proximity. """
|
||||||
|
def __init__(self, hass, zone_friendly_name, dist_to, dir_of_travel,
|
||||||
|
nearest, ignored_zones, proximity_devices, tolerance,
|
||||||
|
proximity_zone):
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
self.hass = hass
|
||||||
|
self.friendly_name = zone_friendly_name
|
||||||
|
self.dist_to = dist_to
|
||||||
|
self.dir_of_travel = dir_of_travel
|
||||||
|
self.nearest = nearest
|
||||||
|
self.ignored_zones = ignored_zones
|
||||||
|
self.proximity_devices = proximity_devices
|
||||||
|
self.tolerance = tolerance
|
||||||
|
self.proximity_zone = proximity_zone
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" Returns the state. """
|
||||||
|
return self.dist_to
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
""" Unit of measurement of this entity. """
|
||||||
|
return "km"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_attributes(self):
|
||||||
|
""" Returns the state attributes. """
|
||||||
|
return {
|
||||||
|
ATTR_DIR_OF_TRAVEL: self.dir_of_travel,
|
||||||
|
ATTR_NEAREST: self.nearest,
|
||||||
|
ATTR_FRIENDLY_NAME: self.friendly_name
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_proximity_state_change(self, entity, old_state, new_state):
|
||||||
|
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
|
||||||
|
""" Function to perform the proximity checking. """
|
||||||
|
entity_name = new_state.name
|
||||||
|
devices_to_calculate = False
|
||||||
|
devices_in_zone = ''
|
||||||
|
|
||||||
|
zone_state = self.hass.states.get(self.proximity_zone)
|
||||||
|
proximity_latitude = zone_state.attributes.get('latitude')
|
||||||
|
proximity_longitude = zone_state.attributes.get('longitude')
|
||||||
|
|
||||||
|
# Check for devices in the monitored zone
|
||||||
|
for device in self.proximity_devices:
|
||||||
|
device_state = self.hass.states.get(device)
|
||||||
|
|
||||||
|
if device_state.state not in self.ignored_zones:
|
||||||
|
devices_to_calculate = True
|
||||||
|
|
||||||
|
# Check the location of all devices
|
||||||
|
if (device_state.state).lower() == (self.friendly_name).lower():
|
||||||
|
device_friendly = device_state.name
|
||||||
|
if devices_in_zone != '':
|
||||||
|
devices_in_zone = devices_in_zone + ', '
|
||||||
|
devices_in_zone = devices_in_zone + device_friendly
|
||||||
|
|
||||||
|
# No-one to track so reset the entity
|
||||||
|
if not devices_to_calculate:
|
||||||
|
self.dist_to = 'not set'
|
||||||
|
self.dir_of_travel = 'not set'
|
||||||
|
self.nearest = 'not set'
|
||||||
|
self.update_ha_state()
|
||||||
|
return
|
||||||
|
|
||||||
|
# At least one device is in the monitored zone so update the entity
|
||||||
|
if devices_in_zone != '':
|
||||||
|
self.dist_to = 0
|
||||||
|
self.dir_of_travel = 'arrived'
|
||||||
|
self.nearest = devices_in_zone
|
||||||
|
self.update_ha_state()
|
||||||
|
return
|
||||||
|
|
||||||
|
# We can't check proximity because latitude and longitude don't exist
|
||||||
|
if 'latitude' not in new_state.attributes:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Collect distances to the zone for all devices
|
||||||
|
distances_to_zone = {}
|
||||||
|
for device in self.proximity_devices:
|
||||||
|
# Ignore devices in an ignored zone
|
||||||
|
device_state = self.hass.states.get(device)
|
||||||
|
if device_state.state in self.ignored_zones:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ignore devices if proximity cannot be calculated
|
||||||
|
if 'latitude' not in device_state.attributes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Calculate the distance to the proximity zone
|
||||||
|
dist_to_zone = distance(proximity_latitude,
|
||||||
|
proximity_longitude,
|
||||||
|
device_state.attributes['latitude'],
|
||||||
|
device_state.attributes['longitude'])
|
||||||
|
|
||||||
|
# Add the device and distance to a dictionary
|
||||||
|
distances_to_zone[device] = round(dist_to_zone / 1000, 1)
|
||||||
|
|
||||||
|
# Loop through each of the distances collected and work out the closest
|
||||||
|
closest_device = ''
|
||||||
|
dist_to_zone = 1000000
|
||||||
|
|
||||||
|
for device in distances_to_zone:
|
||||||
|
if distances_to_zone[device] < dist_to_zone:
|
||||||
|
closest_device = device
|
||||||
|
dist_to_zone = distances_to_zone[device]
|
||||||
|
|
||||||
|
# If the closest device is one of the other devices
|
||||||
|
if closest_device != entity:
|
||||||
|
self.dist_to = round(distances_to_zone[closest_device])
|
||||||
|
self.dir_of_travel = 'unknown'
|
||||||
|
device_state = self.hass.states.get(closest_device)
|
||||||
|
self.nearest = device_state.name
|
||||||
|
self.update_ha_state()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Stop if we cannot calculate the direction of travel (i.e. we don't
|
||||||
|
# have a previous state and a current LAT and LONG)
|
||||||
|
if old_state is None or 'latitude' not in old_state.attributes:
|
||||||
|
self.dist_to = round(distances_to_zone[entity])
|
||||||
|
self.dir_of_travel = 'unknown'
|
||||||
|
self.nearest = entity_name
|
||||||
|
self.update_ha_state()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Reset the variables
|
||||||
|
distance_travelled = 0
|
||||||
|
|
||||||
|
# Calculate the distance travelled
|
||||||
|
old_distance = distance(proximity_latitude, proximity_longitude,
|
||||||
|
old_state.attributes['latitude'],
|
||||||
|
old_state.attributes['longitude'])
|
||||||
|
new_distance = distance(proximity_latitude, proximity_longitude,
|
||||||
|
new_state.attributes['latitude'],
|
||||||
|
new_state.attributes['longitude'])
|
||||||
|
distance_travelled = round(new_distance - old_distance, 1)
|
||||||
|
|
||||||
|
# Check for tolerance
|
||||||
|
if distance_travelled < self.tolerance * -1:
|
||||||
|
direction_of_travel = 'towards'
|
||||||
|
elif distance_travelled > self.tolerance:
|
||||||
|
direction_of_travel = 'away_from'
|
||||||
|
else:
|
||||||
|
direction_of_travel = 'stationary'
|
||||||
|
|
||||||
|
# Update the proximity entity
|
||||||
|
self.dist_to = round(dist_to_zone)
|
||||||
|
self.dir_of_travel = direction_of_travel
|
||||||
|
self.nearest = entity_name
|
||||||
|
self.update_ha_state()
|
||||||
|
_LOGGER.debug('proximity.%s update entity: distance=%s: direction=%s: '
|
||||||
|
'device=%s', self.friendly_name, round(dist_to_zone),
|
||||||
|
direction_of_travel, entity_name)
|
||||||
|
|
||||||
|
_LOGGER.info('%s: proximity calculation complete', entity_name)
|
@ -226,22 +226,28 @@ class Recorder(threading.Thread):
|
|||||||
# State got deleted
|
# State got deleted
|
||||||
if state is None:
|
if state is None:
|
||||||
state_state = ''
|
state_state = ''
|
||||||
|
state_domain = ''
|
||||||
state_attr = '{}'
|
state_attr = '{}'
|
||||||
last_changed = last_updated = now
|
last_changed = last_updated = now
|
||||||
else:
|
else:
|
||||||
|
state_domain = state.domain
|
||||||
state_state = state.state
|
state_state = state.state
|
||||||
state_attr = json.dumps(state.attributes)
|
state_attr = json.dumps(dict(state.attributes))
|
||||||
last_changed = state.last_changed
|
last_changed = state.last_changed
|
||||||
last_updated = state.last_updated
|
last_updated = state.last_updated
|
||||||
|
|
||||||
info = (
|
info = (
|
||||||
entity_id, state_state, state_attr, last_changed, last_updated,
|
entity_id, state_domain, state_state, state_attr,
|
||||||
|
last_changed, last_updated,
|
||||||
now, self.utc_offset, event_id)
|
now, self.utc_offset, event_id)
|
||||||
|
|
||||||
self.query(
|
self.query(
|
||||||
"INSERT INTO states ("
|
"""
|
||||||
"entity_id, state, attributes, last_changed, last_updated,"
|
INSERT INTO states (
|
||||||
"created, utc_offset, event_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
entity_id, domain, state, attributes, last_changed, last_updated,
|
||||||
|
created, utc_offset, event_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
info)
|
info)
|
||||||
|
|
||||||
def record_event(self, event):
|
def record_event(self, event):
|
||||||
@ -279,7 +285,8 @@ class Recorder(threading.Thread):
|
|||||||
else:
|
else:
|
||||||
return cur.fetchall()
|
return cur.fetchall()
|
||||||
|
|
||||||
except sqlite3.IntegrityError:
|
except (sqlite3.IntegrityError, sqlite3.OperationalError,
|
||||||
|
sqlite3.ProgrammingError):
|
||||||
_LOGGER.exception(
|
_LOGGER.exception(
|
||||||
"Error querying the database using: %s", sql_query)
|
"Error querying the database using: %s", sql_query)
|
||||||
return []
|
return []
|
||||||
@ -323,7 +330,7 @@ class Recorder(threading.Thread):
|
|||||||
migration_id = 0
|
migration_id = 0
|
||||||
|
|
||||||
if migration_id < 1:
|
if migration_id < 1:
|
||||||
self.query("""
|
cur.execute("""
|
||||||
CREATE TABLE recorder_runs (
|
CREATE TABLE recorder_runs (
|
||||||
run_id integer primary key,
|
run_id integer primary key,
|
||||||
start integer,
|
start integer,
|
||||||
@ -332,7 +339,7 @@ class Recorder(threading.Thread):
|
|||||||
created integer)
|
created integer)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
self.query("""
|
cur.execute("""
|
||||||
CREATE TABLE events (
|
CREATE TABLE events (
|
||||||
event_id integer primary key,
|
event_id integer primary key,
|
||||||
event_type text,
|
event_type text,
|
||||||
@ -340,10 +347,10 @@ class Recorder(threading.Thread):
|
|||||||
origin text,
|
origin text,
|
||||||
created integer)
|
created integer)
|
||||||
""")
|
""")
|
||||||
self.query(
|
cur.execute(
|
||||||
'CREATE INDEX events__event_type ON events(event_type)')
|
'CREATE INDEX events__event_type ON events(event_type)')
|
||||||
|
|
||||||
self.query("""
|
cur.execute("""
|
||||||
CREATE TABLE states (
|
CREATE TABLE states (
|
||||||
state_id integer primary key,
|
state_id integer primary key,
|
||||||
entity_id text,
|
entity_id text,
|
||||||
@ -353,57 +360,90 @@ class Recorder(threading.Thread):
|
|||||||
last_updated integer,
|
last_updated integer,
|
||||||
created integer)
|
created integer)
|
||||||
""")
|
""")
|
||||||
self.query('CREATE INDEX states__entity_id ON states(entity_id)')
|
cur.execute('CREATE INDEX states__entity_id ON states(entity_id)')
|
||||||
|
|
||||||
save_migration(1)
|
save_migration(1)
|
||||||
|
|
||||||
if migration_id < 2:
|
if migration_id < 2:
|
||||||
self.query("""
|
cur.execute("""
|
||||||
ALTER TABLE events
|
ALTER TABLE events
|
||||||
ADD COLUMN time_fired integer
|
ADD COLUMN time_fired integer
|
||||||
""")
|
""")
|
||||||
|
|
||||||
self.query('UPDATE events SET time_fired=created')
|
cur.execute('UPDATE events SET time_fired=created')
|
||||||
|
|
||||||
save_migration(2)
|
save_migration(2)
|
||||||
|
|
||||||
if migration_id < 3:
|
if migration_id < 3:
|
||||||
utc_offset = self.utc_offset
|
utc_offset = self.utc_offset
|
||||||
|
|
||||||
self.query("""
|
cur.execute("""
|
||||||
ALTER TABLE recorder_runs
|
ALTER TABLE recorder_runs
|
||||||
ADD COLUMN utc_offset integer
|
ADD COLUMN utc_offset integer
|
||||||
""")
|
""")
|
||||||
|
|
||||||
self.query("""
|
cur.execute("""
|
||||||
ALTER TABLE events
|
ALTER TABLE events
|
||||||
ADD COLUMN utc_offset integer
|
ADD COLUMN utc_offset integer
|
||||||
""")
|
""")
|
||||||
|
|
||||||
self.query("""
|
cur.execute("""
|
||||||
ALTER TABLE states
|
ALTER TABLE states
|
||||||
ADD COLUMN utc_offset integer
|
ADD COLUMN utc_offset integer
|
||||||
""")
|
""")
|
||||||
|
|
||||||
self.query("UPDATE recorder_runs SET utc_offset=?", [utc_offset])
|
cur.execute("UPDATE recorder_runs SET utc_offset=?", [utc_offset])
|
||||||
self.query("UPDATE events SET utc_offset=?", [utc_offset])
|
cur.execute("UPDATE events SET utc_offset=?", [utc_offset])
|
||||||
self.query("UPDATE states SET utc_offset=?", [utc_offset])
|
cur.execute("UPDATE states SET utc_offset=?", [utc_offset])
|
||||||
|
|
||||||
save_migration(3)
|
save_migration(3)
|
||||||
|
|
||||||
if migration_id < 4:
|
if migration_id < 4:
|
||||||
# We had a bug where we did not save utc offset for recorder runs
|
# We had a bug where we did not save utc offset for recorder runs
|
||||||
self.query(
|
cur.execute(
|
||||||
"""UPDATE recorder_runs SET utc_offset=?
|
"""UPDATE recorder_runs SET utc_offset=?
|
||||||
WHERE utc_offset IS NULL""", [self.utc_offset])
|
WHERE utc_offset IS NULL""", [self.utc_offset])
|
||||||
|
|
||||||
self.query("""
|
cur.execute("""
|
||||||
ALTER TABLE states
|
ALTER TABLE states
|
||||||
ADD COLUMN event_id integer
|
ADD COLUMN event_id integer
|
||||||
""")
|
""")
|
||||||
|
|
||||||
save_migration(4)
|
save_migration(4)
|
||||||
|
|
||||||
|
if migration_id < 5:
|
||||||
|
# Add domain so that thermostat graphs look right
|
||||||
|
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
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE states
|
||||||
|
set domain=substr(entity_id, 0, instr(entity_id, '.'))
|
||||||
|
""")
|
||||||
|
|
||||||
|
# add indexes we are going to use a lot on selects
|
||||||
|
cur.execute("""
|
||||||
|
CREATE INDEX states__state_changes ON
|
||||||
|
states (last_changed, last_updated, entity_id)""")
|
||||||
|
cur.execute("""
|
||||||
|
CREATE INDEX states__significant_changes ON
|
||||||
|
states (domain, last_updated, entity_id)""")
|
||||||
|
save_migration(5)
|
||||||
|
|
||||||
def _close_connection(self):
|
def _close_connection(self):
|
||||||
""" Close connection to the database. """
|
""" Close connection to the database. """
|
||||||
_LOGGER.info("Closing database")
|
_LOGGER.info("Closing database")
|
||||||
|
@ -9,8 +9,8 @@ https://home-assistant.io/components/rfxtrx/
|
|||||||
import logging
|
import logging
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/0.2.zip' +
|
REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/0.4.zip' +
|
||||||
'#RFXtrx==0.2']
|
'#RFXtrx==0.4']
|
||||||
|
|
||||||
DOMAIN = "rfxtrx"
|
DOMAIN = "rfxtrx"
|
||||||
|
|
||||||
@ -37,6 +37,8 @@ def setup(hass, config):
|
|||||||
""" Callback all subscribers for RFXtrx gateway. """
|
""" Callback all subscribers for RFXtrx gateway. """
|
||||||
|
|
||||||
# Log RFXCOM event
|
# Log RFXCOM event
|
||||||
|
if not event.device.id_string:
|
||||||
|
return
|
||||||
entity_id = slugify(event.device.id_string.lower())
|
entity_id = slugify(event.device.id_string.lower())
|
||||||
packet_id = "".join("{0:02x}".format(x) for x in event.data)
|
packet_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||||
entity_name = "%s : %s" % (entity_id, packet_id)
|
entity_name = "%s : %s" % (entity_id, packet_id)
|
||||||
|
98
homeassistant/components/rollershutter/scsgate.py
Normal file
98
homeassistant/components/rollershutter/scsgate.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.rollershutter.scsgate
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Allows to configure a SCSGate rollershutter.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/rollershutter.scsgate/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import homeassistant.components.scsgate as scsgate
|
||||||
|
from homeassistant.components.rollershutter import RollershutterDevice
|
||||||
|
|
||||||
|
|
||||||
|
DEPENDENCIES = ['scsgate']
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
|
""" Add the SCSGate swiches defined inside of the configuration file. """
|
||||||
|
|
||||||
|
devices = config.get('devices')
|
||||||
|
rollershutters = []
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if devices:
|
||||||
|
for _, entity_info in devices.items():
|
||||||
|
if entity_info['scs_id'] in scsgate.SCSGATE.devices:
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info("Adding %s scsgate.rollershutter", entity_info['name'])
|
||||||
|
|
||||||
|
name = entity_info['name']
|
||||||
|
scs_id = entity_info['scs_id']
|
||||||
|
rollershutter = SCSGateRollerShutter(
|
||||||
|
name=name,
|
||||||
|
scs_id=scs_id,
|
||||||
|
logger=logger)
|
||||||
|
scsgate.SCSGATE.add_device(rollershutter)
|
||||||
|
rollershutters.append(rollershutter)
|
||||||
|
|
||||||
|
add_devices_callback(rollershutters)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||||
|
class SCSGateRollerShutter(RollershutterDevice):
|
||||||
|
""" Represents a rollershutter that can be controlled using SCSGate. """
|
||||||
|
def __init__(self, scs_id, name, logger):
|
||||||
|
self._scs_id = scs_id
|
||||||
|
self._name = name
|
||||||
|
self._logger = logger
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scs_id(self):
|
||||||
|
""" SCSGate ID """
|
||||||
|
return self._scs_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
""" No polling needed """
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" The name of the rollershutter. """
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_position(self):
|
||||||
|
"""
|
||||||
|
Return current position of rollershutter.
|
||||||
|
None is unknown, 0 is closed, 100 is fully open.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def move_up(self, **kwargs):
|
||||||
|
""" Move the rollershutter up. """
|
||||||
|
from scsgate.tasks import RaiseRollerShutterTask
|
||||||
|
|
||||||
|
scsgate.SCSGATE.append_task(
|
||||||
|
RaiseRollerShutterTask(target=self._scs_id))
|
||||||
|
|
||||||
|
def move_down(self, **kwargs):
|
||||||
|
""" Move the rollershutter down. """
|
||||||
|
from scsgate.tasks import LowerRollerShutterTask
|
||||||
|
|
||||||
|
scsgate.SCSGATE.append_task(
|
||||||
|
LowerRollerShutterTask(target=self._scs_id))
|
||||||
|
|
||||||
|
def stop(self, **kwargs):
|
||||||
|
""" Stop the device. """
|
||||||
|
from scsgate.tasks import HaltRollerShutterTask
|
||||||
|
|
||||||
|
scsgate.SCSGATE.append_task(HaltRollerShutterTask(target=self._scs_id))
|
||||||
|
|
||||||
|
def process_event(self, message):
|
||||||
|
""" Handle a SCSGate message related with this rollershutter """
|
||||||
|
self._logger.debug(
|
||||||
|
"Rollershutter %s, got message %s",
|
||||||
|
self._scs_id, message.toggled)
|
156
homeassistant/components/scsgate.py
Normal file
156
homeassistant/components/scsgate.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.scsgate
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Provides support for SCSGate components.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/scsgate/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from threading import Lock
|
||||||
|
from homeassistant.core import EVENT_HOMEASSISTANT_STOP
|
||||||
|
|
||||||
|
REQUIREMENTS = ['scsgate==0.1.0']
|
||||||
|
DOMAIN = "scsgate"
|
||||||
|
SCSGATE = None
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SCSGate:
|
||||||
|
""" Class dealing with the SCSGate device via scsgate.Reactor. """
|
||||||
|
|
||||||
|
def __init__(self, device, logger):
|
||||||
|
self._logger = logger
|
||||||
|
self._devices = {}
|
||||||
|
self._devices_to_register = {}
|
||||||
|
self._devices_to_register_lock = Lock()
|
||||||
|
self._device_being_registered = None
|
||||||
|
self._device_being_registered_lock = Lock()
|
||||||
|
|
||||||
|
from scsgate.connection import Connection
|
||||||
|
connection = Connection(device=device, logger=self._logger)
|
||||||
|
|
||||||
|
from scsgate.reactor import Reactor
|
||||||
|
self._reactor = Reactor(
|
||||||
|
connection=connection,
|
||||||
|
logger=self._logger,
|
||||||
|
handle_message=self.handle_message)
|
||||||
|
|
||||||
|
def handle_message(self, message):
|
||||||
|
""" Method called whenever a message is seen on the bus. """
|
||||||
|
from scsgate.messages import StateMessage, ScenarioTriggeredMessage
|
||||||
|
|
||||||
|
self._logger.debug("Received message {}".format(message))
|
||||||
|
if not isinstance(message, StateMessage) and \
|
||||||
|
not isinstance(message, ScenarioTriggeredMessage):
|
||||||
|
msg = "Ignored message {} - not releavant type".format(
|
||||||
|
message)
|
||||||
|
self._logger.debug(msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
if message.entity in self._devices:
|
||||||
|
new_device_activated = False
|
||||||
|
with self._devices_to_register_lock:
|
||||||
|
if message.entity == self._device_being_registered:
|
||||||
|
self._device_being_registered = None
|
||||||
|
new_device_activated = True
|
||||||
|
if new_device_activated:
|
||||||
|
self._activate_next_device()
|
||||||
|
|
||||||
|
# pylint: disable=broad-except
|
||||||
|
try:
|
||||||
|
self._devices[message.entity].process_event(message)
|
||||||
|
except Exception as exception:
|
||||||
|
msg = "Exception while processing event: {}".format(
|
||||||
|
exception)
|
||||||
|
self._logger.error(msg)
|
||||||
|
else:
|
||||||
|
self._logger.info(
|
||||||
|
"Ignoring state message for device {} because unknonw".format(
|
||||||
|
message.entity))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def devices(self):
|
||||||
|
"""
|
||||||
|
Dictionary with known devices. Key is device ID, value is the device
|
||||||
|
itself.
|
||||||
|
"""
|
||||||
|
return self._devices
|
||||||
|
|
||||||
|
def add_device(self, device):
|
||||||
|
"""
|
||||||
|
Adds the specified device to the list of the already registered ones.
|
||||||
|
|
||||||
|
Beware: this is not what you usually want to do, take a look at
|
||||||
|
`add_devices_to_register`
|
||||||
|
"""
|
||||||
|
self._devices[device.scs_id] = device
|
||||||
|
|
||||||
|
def add_devices_to_register(self, devices):
|
||||||
|
""" List of devices to be registered. """
|
||||||
|
with self._devices_to_register_lock:
|
||||||
|
for device in devices:
|
||||||
|
self._devices_to_register[device.scs_id] = device
|
||||||
|
self._activate_next_device()
|
||||||
|
|
||||||
|
def _activate_next_device(self):
|
||||||
|
""" Starts the activation of the first device. """
|
||||||
|
from scsgate.tasks import GetStatusTask
|
||||||
|
|
||||||
|
with self._devices_to_register_lock:
|
||||||
|
if len(self._devices_to_register) == 0:
|
||||||
|
return
|
||||||
|
_, device = self._devices_to_register.popitem()
|
||||||
|
self._devices[device.scs_id] = device
|
||||||
|
self._device_being_registered = device.scs_id
|
||||||
|
self._reactor.append_task(GetStatusTask(target=device.scs_id))
|
||||||
|
|
||||||
|
def is_device_registered(self, device_id):
|
||||||
|
""" Checks whether a device is already registered or not. """
|
||||||
|
with self._devices_to_register_lock:
|
||||||
|
if device_id in self._devices_to_register.keys():
|
||||||
|
return False
|
||||||
|
|
||||||
|
with self._device_being_registered_lock:
|
||||||
|
if device_id == self._device_being_registered:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
""" Start the scsgate.Reactor. """
|
||||||
|
self._reactor.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
""" Stop the scsgate.Reactor. """
|
||||||
|
self._reactor.stop()
|
||||||
|
|
||||||
|
def append_task(self, task):
|
||||||
|
""" Registers a new task to be executed. """
|
||||||
|
self._reactor.append_task(task)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
""" Setup the SCSGate component. """
|
||||||
|
device = config['scsgate']['device']
|
||||||
|
global SCSGATE
|
||||||
|
|
||||||
|
# pylint: disable=broad-except
|
||||||
|
try:
|
||||||
|
SCSGATE = SCSGate(device=device, logger=_LOGGER)
|
||||||
|
SCSGATE.start()
|
||||||
|
except Exception as exception:
|
||||||
|
_LOGGER.error("Cannot setup SCSGate component: %s", exception)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop_monitor(event):
|
||||||
|
"""
|
||||||
|
Invoked when home-assistant is exiting. Performs the necessary
|
||||||
|
cleanups.
|
||||||
|
"""
|
||||||
|
_LOGGER.info("Stopping SCSGate monitor thread")
|
||||||
|
SCSGATE.stop()
|
||||||
|
|
||||||
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_monitor)
|
||||||
|
|
||||||
|
return True
|
90
homeassistant/components/sensor/apcupsd.py
Normal file
90
homeassistant/components/sensor/apcupsd.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.sensor.apcupsd
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Provides a sensor to track various status aspects of a UPS.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.apcupsd/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.const import TEMP_CELCIUS
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.components import apcupsd
|
||||||
|
|
||||||
|
DEPENDENCIES = [apcupsd.DOMAIN]
|
||||||
|
DEFAULT_NAME = "UPS Status"
|
||||||
|
SPECIFIC_UNITS = {
|
||||||
|
"ITEMP": TEMP_CELCIUS
|
||||||
|
}
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
"""
|
||||||
|
Ensure that the 'type' config value has been set and use a specific unit
|
||||||
|
of measurement if required.
|
||||||
|
"""
|
||||||
|
typ = config.get(apcupsd.CONF_TYPE)
|
||||||
|
if typ is None:
|
||||||
|
_LOGGER.error(
|
||||||
|
"You must include a '%s' when configuring an APCUPSd sensor.",
|
||||||
|
apcupsd.CONF_TYPE)
|
||||||
|
return False
|
||||||
|
typ = typ.upper()
|
||||||
|
|
||||||
|
if typ not in apcupsd.DATA.status:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Specified '%s' of '%s' does not appear in the APCUPSd status "
|
||||||
|
"output.", apcupsd.CONF_TYPE, typ)
|
||||||
|
return False
|
||||||
|
|
||||||
|
add_entities((
|
||||||
|
Sensor(config, apcupsd.DATA, unit=SPECIFIC_UNITS.get(typ)),
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def infer_unit(value):
|
||||||
|
"""
|
||||||
|
If the value ends with any of the units from ALL_UNITS, split the unit
|
||||||
|
off the end of the value and return the value, unit tuple pair. Else return
|
||||||
|
the original value and None as the unit.
|
||||||
|
"""
|
||||||
|
from apcaccess.status import ALL_UNITS
|
||||||
|
for unit in ALL_UNITS:
|
||||||
|
if value.endswith(unit):
|
||||||
|
return value[:-len(unit)], unit
|
||||||
|
return value, None
|
||||||
|
|
||||||
|
|
||||||
|
class Sensor(Entity):
|
||||||
|
""" Generic sensor entity for APCUPSd status values. """
|
||||||
|
def __init__(self, config, data, unit=None):
|
||||||
|
self._config = config
|
||||||
|
self._unit = unit
|
||||||
|
self._data = data
|
||||||
|
self._inferred_unit = None
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" The name of the UPS sensor. """
|
||||||
|
return self._config.get("name", DEFAULT_NAME)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" True if the UPS is online, else False. """
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
""" Unit of measurement of this entity, if any. """
|
||||||
|
if self._unit is None:
|
||||||
|
return self._inferred_unit
|
||||||
|
return self._unit
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
""" Get the latest status and use it to update our sensor state. """
|
||||||
|
key = self._config[apcupsd.CONF_TYPE].upper()
|
||||||
|
self._state, self._inferred_unit = infer_unit(self._data.status[key])
|
@ -39,6 +39,7 @@ OPTION_TYPES = {
|
|||||||
'miners_revenue_btc': ['Miners revenue', 'BTC'],
|
'miners_revenue_btc': ['Miners revenue', 'BTC'],
|
||||||
'market_price_usd': ['Market price', 'USD']
|
'market_price_usd': ['Market price', 'USD']
|
||||||
}
|
}
|
||||||
|
ICON = 'mdi:currency-btc'
|
||||||
|
|
||||||
# Return cached results if last scan was less then this time ago
|
# Return cached results if last scan was less then this time ago
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
||||||
@ -108,6 +109,11 @@ class BitcoinSensor(Entity):
|
|||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
return self._unit_of_measurement
|
return self._unit_of_measurement
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
""" Icon to use in the frontend, if any. """
|
||||||
|
return ICON
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
def update(self):
|
def update(self):
|
||||||
""" Gets the latest data and updates the states. """
|
""" Gets the latest data and updates the states. """
|
||||||
|
104
homeassistant/components/sensor/bloomsky.py
Normal file
104
homeassistant/components/sensor/bloomsky.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.sensor.bloomsky
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Support the sensor of a BloomSky weather station.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.bloomsky/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import homeassistant.components.bloomsky as bloomsky
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
DEPENDENCIES = ["bloomsky"]
|
||||||
|
|
||||||
|
# These are the available sensors
|
||||||
|
SENSOR_TYPES = ["Temperature",
|
||||||
|
"Humidity",
|
||||||
|
"Rain",
|
||||||
|
"Pressure",
|
||||||
|
"Luminance",
|
||||||
|
"Night",
|
||||||
|
"UVIndex"]
|
||||||
|
|
||||||
|
# Sensor units - these do not currently align with the API documentation
|
||||||
|
SENSOR_UNITS = {"Temperature": "°F",
|
||||||
|
"Humidity": "%",
|
||||||
|
"Pressure": "inHg",
|
||||||
|
"Luminance": "cd/m²"}
|
||||||
|
|
||||||
|
# Which sensors to format numerically
|
||||||
|
FORMAT_NUMBERS = ["Temperature", "Pressure"]
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Set up the available BloomSky weather sensors. """
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
for device_key in bloomsky.BLOOMSKY.devices:
|
||||||
|
device = bloomsky.BLOOMSKY.devices[device_key]
|
||||||
|
for variable in config["monitored_conditions"]:
|
||||||
|
if variable in SENSOR_TYPES:
|
||||||
|
add_devices([BloomSkySensor(bloomsky.BLOOMSKY,
|
||||||
|
device,
|
||||||
|
variable)])
|
||||||
|
else:
|
||||||
|
logger.error("Cannot find definition for device: %s", variable)
|
||||||
|
|
||||||
|
|
||||||
|
class BloomSkySensor(Entity):
|
||||||
|
""" Represents a single sensor in a BloomSky device. """
|
||||||
|
|
||||||
|
def __init__(self, bs, device, sensor_name):
|
||||||
|
self._bloomsky = bs
|
||||||
|
self._device_id = device["DeviceID"]
|
||||||
|
self._client_name = device["DeviceName"]
|
||||||
|
self._sensor_name = sensor_name
|
||||||
|
self._state = self.process_state(device)
|
||||||
|
self._sensor_update = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" The name of the BloomSky device and this sensor. """
|
||||||
|
return "{} {}".format(self._client_name, self._sensor_name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" The current state (i.e. value) of this sensor. """
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
""" This sensor's units. """
|
||||||
|
return SENSOR_UNITS.get(self._sensor_name, None)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
""" Request an update from the BloomSky API. """
|
||||||
|
self._bloomsky.refresh_devices()
|
||||||
|
# TS is a Unix epoch timestamp for the last time the BloomSky servers
|
||||||
|
# heard from this device. If that value hasn't changed, the value has
|
||||||
|
# not been updated.
|
||||||
|
last_ts = self._bloomsky.devices[self._device_id]["Data"]["TS"]
|
||||||
|
if last_ts != self._sensor_update:
|
||||||
|
self.process_state(self._bloomsky.devices[self._device_id])
|
||||||
|
self._sensor_update = last_ts
|
||||||
|
|
||||||
|
def process_state(self, device):
|
||||||
|
""" Handle the response from the BloomSky API for this sensor. """
|
||||||
|
data = device["Data"][self._sensor_name]
|
||||||
|
if self._sensor_name == "Rain":
|
||||||
|
if data:
|
||||||
|
self._state = "Raining"
|
||||||
|
else:
|
||||||
|
self._state = "Not raining"
|
||||||
|
elif self._sensor_name == "Night":
|
||||||
|
if data:
|
||||||
|
self._state = "Nighttime"
|
||||||
|
else:
|
||||||
|
self._state = "Daytime"
|
||||||
|
elif self._sensor_name in FORMAT_NUMBERS:
|
||||||
|
self._state = "{0:.2f}".format(data)
|
||||||
|
else:
|
||||||
|
self._state = data
|
@ -19,6 +19,7 @@ DEFAULT_NAME = "CPU speed"
|
|||||||
ATTR_VENDOR = 'Vendor ID'
|
ATTR_VENDOR = 'Vendor ID'
|
||||||
ATTR_BRAND = 'Brand'
|
ATTR_BRAND = 'Brand'
|
||||||
ATTR_HZ = 'GHz Advertised'
|
ATTR_HZ = 'GHz Advertised'
|
||||||
|
ICON = 'mdi:pulse'
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-variable
|
# pylint: disable=unused-variable
|
||||||
@ -53,7 +54,7 @@ class CpuSpeedSensor(Entity):
|
|||||||
return self._unit_of_measurement
|
return self._unit_of_measurement
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def device_state_attributes(self):
|
||||||
""" Returns the state attributes. """
|
""" Returns the state attributes. """
|
||||||
if self.info is not None:
|
if self.info is not None:
|
||||||
return {
|
return {
|
||||||
@ -62,6 +63,11 @@ class CpuSpeedSensor(Entity):
|
|||||||
ATTR_HZ: round(self.info['hz_advertised_raw'][0]/10**9, 2)
|
ATTR_HZ: round(self.info['hz_advertised_raw'][0]/10**9, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
""" Icon to use in the frontend, if any. """
|
||||||
|
return ICON
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
""" Gets the latest data and updates the state. """
|
""" Gets the latest data and updates the state. """
|
||||||
from cpuinfo import cpuinfo
|
from cpuinfo import cpuinfo
|
||||||
|
@ -46,7 +46,7 @@ class DemoSensor(Entity):
|
|||||||
return self._unit_of_measurement
|
return self._unit_of_measurement
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def device_state_attributes(self):
|
||||||
""" Returns the state attributes. """
|
""" Returns the state attributes. """
|
||||||
if self._battery:
|
if self._battery:
|
||||||
return {
|
return {
|
||||||
|
@ -23,6 +23,7 @@ SENSOR_TYPES = {
|
|||||||
'temperature': ['Temperature', None],
|
'temperature': ['Temperature', None],
|
||||||
'humidity': ['Humidity', '%']
|
'humidity': ['Humidity', '%']
|
||||||
}
|
}
|
||||||
|
DEFAULT_NAME = "DHT Sensor"
|
||||||
# Return cached results if last scan was less then this time ago
|
# Return cached results if last scan was less then this time ago
|
||||||
# DHT11 is able to deliver data once per second, DHT22 once every two
|
# DHT11 is able to deliver data once per second, DHT22 once every two
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||||
@ -53,12 +54,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
data = DHTClient(Adafruit_DHT, sensor, pin)
|
data = DHTClient(Adafruit_DHT, sensor, pin)
|
||||||
dev = []
|
dev = []
|
||||||
|
name = config.get('name', DEFAULT_NAME)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for variable in config['monitored_conditions']:
|
for variable in config['monitored_conditions']:
|
||||||
if variable not in SENSOR_TYPES:
|
if variable not in SENSOR_TYPES:
|
||||||
_LOGGER.error('Sensor type: "%s" does not exist', variable)
|
_LOGGER.error('Sensor type: "%s" does not exist', variable)
|
||||||
else:
|
else:
|
||||||
dev.append(DHTSensor(data, variable, unit))
|
dev.append(DHTSensor(data, variable, unit, name))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -69,8 +72,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
class DHTSensor(Entity):
|
class DHTSensor(Entity):
|
||||||
""" Implements an DHT sensor. """
|
""" Implements an DHT sensor. """
|
||||||
|
|
||||||
def __init__(self, dht_client, sensor_type, temp_unit):
|
def __init__(self, dht_client, sensor_type, temp_unit, name):
|
||||||
self.client_name = 'DHT sensor'
|
self.client_name = name
|
||||||
self._name = SENSOR_TYPES[sensor_type][0]
|
self._name = SENSOR_TYPES[sensor_type][0]
|
||||||
self.dht_client = dht_client
|
self.dht_client = dht_client
|
||||||
self.temp_unit = temp_unit
|
self.temp_unit = temp_unit
|
||||||
|
@ -1,29 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
homeassistant.components.sensor.ecobee
|
homeassistant.components.sensor.ecobee
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Sensor platform for Ecobee sensors.
|
||||||
|
|
||||||
Ecobee Thermostat Component
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.ecobee/
|
||||||
This component adds support for Ecobee3 Wireless Thermostats.
|
|
||||||
You will need to setup developer access to your thermostat,
|
|
||||||
and create and API key on the ecobee website.
|
|
||||||
|
|
||||||
The first time you run this component you will see a configuration
|
|
||||||
component card in Home Assistant. This card will contain a PIN code
|
|
||||||
that you will need to use to authorize access to your thermostat. You
|
|
||||||
can do this at https://www.ecobee.com/consumerportal/index.html
|
|
||||||
Click My Apps, Add application, Enter Pin and click Authorize.
|
|
||||||
|
|
||||||
After authorizing the application click the button in the configuration
|
|
||||||
card. Now your thermostat and sensors should shown in home-assistant.
|
|
||||||
|
|
||||||
You can use the optional hold_temp parameter to set whether or not holds
|
|
||||||
are set indefintely or until the next scheduled event.
|
|
||||||
|
|
||||||
ecobee:
|
|
||||||
api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf
|
|
||||||
hold_temp: True
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -32,7 +13,6 @@ from homeassistant.components import ecobee
|
|||||||
from homeassistant.const import TEMP_FAHRENHEIT
|
from homeassistant.const import TEMP_FAHRENHEIT
|
||||||
|
|
||||||
DEPENDENCIES = ['ecobee']
|
DEPENDENCIES = ['ecobee']
|
||||||
|
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
'temperature': ['Temperature', TEMP_FAHRENHEIT],
|
'temperature': ['Temperature', TEMP_FAHRENHEIT],
|
||||||
'humidity': ['Humidity', '%'],
|
'humidity': ['Humidity', '%'],
|
||||||
@ -40,12 +20,11 @@ SENSOR_TYPES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ECOBEE_CONFIG_FILE = 'ecobee.conf'
|
ECOBEE_CONFIG_FILE = 'ecobee.conf'
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
""" Sets up the sensors. """
|
""" Sets up the Ecobee sensors. """
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
data = ecobee.NETWORK
|
data = ecobee.NETWORK
|
||||||
@ -63,7 +42,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
|
|
||||||
class EcobeeSensor(Entity):
|
class EcobeeSensor(Entity):
|
||||||
""" An ecobee sensor. """
|
""" An Ecobee sensor. """
|
||||||
|
|
||||||
def __init__(self, sensor_name, sensor_type, sensor_index):
|
def __init__(self, sensor_name, sensor_type, sensor_index):
|
||||||
self._name = sensor_name + ' ' + SENSOR_TYPES[sensor_type][0]
|
self._name = sensor_name + ' ' + SENSOR_TYPES[sensor_type][0]
|
||||||
@ -76,6 +55,7 @@ class EcobeeSensor(Entity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
""" Returns the name of the Ecobee sensor.. """
|
||||||
return self._name.rstrip()
|
return self._name.rstrip()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -83,11 +63,18 @@ class EcobeeSensor(Entity):
|
|||||||
""" Returns the state of the device. """
|
""" Returns the state of the device. """
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Unique id of this sensor."""
|
||||||
|
return "sensor_ecobee_{}_{}".format(self._name, self.index)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
|
""" Unit of measurement this sensor expresses itself in. """
|
||||||
return self._unit_of_measurement
|
return self._unit_of_measurement
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
|
""" Get the latest state of the sensor. """
|
||||||
data = ecobee.NETWORK
|
data = ecobee.NETWORK
|
||||||
data.update()
|
data.update()
|
||||||
for sensor in data.ecobee.get_remote_sensors(self.index):
|
for sensor in data.ecobee.get_remote_sensors(self.index):
|
||||||
|
99
homeassistant/components/sensor/mfi.py
Normal file
99
homeassistant/components/sensor/mfi.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.sensor.mfi
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Support for Ubiquiti mFi sensors.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.mfi/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD,
|
||||||
|
TEMP_CELCIUS)
|
||||||
|
from homeassistant.components.sensor import DOMAIN
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.helpers import validate_config
|
||||||
|
|
||||||
|
REQUIREMENTS = ['mficlient==0.2.2']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STATE_ON = 'on'
|
||||||
|
STATE_OFF = 'off'
|
||||||
|
DIGITS = {
|
||||||
|
'volts': 1,
|
||||||
|
'amps': 1,
|
||||||
|
'active_power': 0,
|
||||||
|
'temperature': 1,
|
||||||
|
}
|
||||||
|
SENSOR_MODELS = [
|
||||||
|
'Ubiquiti mFi-THS',
|
||||||
|
'Ubiquiti mFi-CS',
|
||||||
|
'Outlet',
|
||||||
|
'Input Analog',
|
||||||
|
'Input Digital',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-variable
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Sets up mFi sensors. """
|
||||||
|
|
||||||
|
if not validate_config({DOMAIN: config},
|
||||||
|
{DOMAIN: ['host',
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_PASSWORD]},
|
||||||
|
_LOGGER):
|
||||||
|
_LOGGER.error('A host, username, and password are required')
|
||||||
|
return False
|
||||||
|
|
||||||
|
host = config.get('host')
|
||||||
|
port = int(config.get('port', 6443))
|
||||||
|
username = config.get(CONF_USERNAME)
|
||||||
|
password = config.get(CONF_PASSWORD)
|
||||||
|
|
||||||
|
from mficlient.client import MFiClient
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = MFiClient(host, username, password, port=port)
|
||||||
|
except client.FailedToLogin as ex:
|
||||||
|
_LOGGER.error('Unable to connect to mFi: %s', str(ex))
|
||||||
|
return False
|
||||||
|
|
||||||
|
add_devices(MfiSensor(port, hass)
|
||||||
|
for device in client.get_devices()
|
||||||
|
for port in device.ports.values()
|
||||||
|
if port.model in SENSOR_MODELS)
|
||||||
|
|
||||||
|
|
||||||
|
class MfiSensor(Entity):
|
||||||
|
""" An mFi sensor that exposes tag=value. """
|
||||||
|
|
||||||
|
def __init__(self, port, hass):
|
||||||
|
self._port = port
|
||||||
|
self._hass = hass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self._port.label
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
if self._port.model == 'Input Digital':
|
||||||
|
return self._port.value > 0 and STATE_ON or STATE_OFF
|
||||||
|
else:
|
||||||
|
digits = DIGITS.get(self._port.tag, 0)
|
||||||
|
return round(self._port.value, digits)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
if self._port.tag == 'temperature':
|
||||||
|
return TEMP_CELCIUS
|
||||||
|
elif self._port.tag == 'active_pwr':
|
||||||
|
return 'Watts'
|
||||||
|
elif self._port.model == 'Input Digital':
|
||||||
|
return 'State'
|
||||||
|
return self._port.tag
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self._port.refresh()
|
@ -7,7 +7,7 @@ For more details about this platform, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/sensor.mqtt/
|
https://home-assistant.io/components/sensor.mqtt/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
from homeassistant.const import CONF_VALUE_TEMPLATE, STATE_UNKNOWN
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.util import template
|
from homeassistant.util import template
|
||||||
import homeassistant.components.mqtt as mqtt
|
import homeassistant.components.mqtt as mqtt
|
||||||
@ -42,7 +42,7 @@ class MqttSensor(Entity):
|
|||||||
""" Represents a sensor that can be updated using MQTT. """
|
""" Represents a sensor that can be updated using MQTT. """
|
||||||
def __init__(self, hass, name, state_topic, qos, unit_of_measurement,
|
def __init__(self, hass, name, state_topic, qos, unit_of_measurement,
|
||||||
value_template):
|
value_template):
|
||||||
self._state = "-"
|
self._state = STATE_UNKNOWN
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state_topic = state_topic
|
self._state_topic = state_topic
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
homeassistant.components.sensor.mysensors
|
homeassistant.components.sensor.mysensors.
|
||||||
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Support for MySensors sensors.
|
Support for MySensors sensors.
|
||||||
|
|
||||||
@ -30,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
for gateway in mysensors.GATEWAYS.values():
|
for gateway in mysensors.GATEWAYS.values():
|
||||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||||
# states. Map them in a defaultdict(list).
|
# states. Map them in a dict of lists.
|
||||||
pres = gateway.const.Presentation
|
pres = gateway.const.Presentation
|
||||||
set_req = gateway.const.SetReq
|
set_req = gateway.const.SetReq
|
||||||
map_sv_types = {
|
map_sv_types = {
|
||||||
@ -151,48 +152,46 @@ class MySensorsSensor(Entity):
|
|||||||
self.gateway.const.SetReq.V_VOLTAGE: 'V',
|
self.gateway.const.SetReq.V_VOLTAGE: 'V',
|
||||||
self.gateway.const.SetReq.V_CURRENT: 'A',
|
self.gateway.const.SetReq.V_CURRENT: 'A',
|
||||||
}
|
}
|
||||||
unit_map_v15 = {
|
|
||||||
self.gateway.const.SetReq.V_PERCENTAGE: '%',
|
|
||||||
}
|
|
||||||
if float(self.gateway.version) >= 1.5:
|
if float(self.gateway.version) >= 1.5:
|
||||||
if self.gateway.const.SetReq.V_UNIT_PREFIX in self._values:
|
if self.gateway.const.SetReq.V_UNIT_PREFIX in self._values:
|
||||||
return self._values[
|
return self._values[
|
||||||
self.gateway.const.SetReq.V_UNIT_PREFIX]
|
self.gateway.const.SetReq.V_UNIT_PREFIX]
|
||||||
unit_map.update(unit_map_v15)
|
unit_map.update({self.gateway.const.SetReq.V_PERCENTAGE: '%'})
|
||||||
return unit_map.get(self.value_type)
|
return unit_map.get(self.value_type)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return device specific state attributes."""
|
"""Return device specific state attributes."""
|
||||||
device_attr = {}
|
attr = {
|
||||||
for value_type, value in self._values.items():
|
|
||||||
if value_type != self.value_type:
|
|
||||||
device_attr[self.gateway.const.SetReq(value_type).name] = value
|
|
||||||
return device_attr
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state_attributes(self):
|
|
||||||
"""Return the state attributes."""
|
|
||||||
data = {
|
|
||||||
mysensors.ATTR_PORT: self.gateway.port,
|
mysensors.ATTR_PORT: self.gateway.port,
|
||||||
mysensors.ATTR_NODE_ID: self.node_id,
|
mysensors.ATTR_NODE_ID: self.node_id,
|
||||||
mysensors.ATTR_CHILD_ID: self.child_id,
|
mysensors.ATTR_CHILD_ID: self.child_id,
|
||||||
ATTR_BATTERY_LEVEL: self.battery_level,
|
ATTR_BATTERY_LEVEL: self.battery_level,
|
||||||
}
|
}
|
||||||
|
|
||||||
device_attr = self.device_state_attributes
|
set_req = self.gateway.const.SetReq
|
||||||
|
|
||||||
if device_attr is not None:
|
for value_type, value in self._values.items():
|
||||||
data.update(device_attr)
|
if value_type != self.value_type:
|
||||||
|
try:
|
||||||
|
attr[set_req(value_type).name] = value
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.error('value_type %s is not valid for mysensors '
|
||||||
|
'version %s', value_type,
|
||||||
|
self.gateway.version)
|
||||||
|
return attr
|
||||||
|
|
||||||
return data
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return self.value_type in self._values
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Update the controller with the latest values from a sensor."""
|
"""Update the controller with the latest values from a sensor."""
|
||||||
node = self.gateway.sensors[self.node_id]
|
node = self.gateway.sensors[self.node_id]
|
||||||
child = node.children[self.child_id]
|
child = node.children[self.child_id]
|
||||||
for value_type, value in child.values.items():
|
for value_type, value in child.values.items():
|
||||||
_LOGGER.info(
|
_LOGGER.debug(
|
||||||
"%s: value_type %s, value = %s", self._name, value_type, value)
|
"%s: value_type %s, value = %s", self._name, value_type, value)
|
||||||
if value_type == self.gateway.const.SetReq.V_TRIPPED:
|
if value_type == self.gateway.const.SetReq.V_TRIPPED:
|
||||||
self._values[value_type] = STATE_ON if int(
|
self._values[value_type] = STATE_ON if int(
|
||||||
|
@ -21,7 +21,7 @@ SENSOR_TYPES = ['humidity',
|
|||||||
'last_connection',
|
'last_connection',
|
||||||
'battery_level']
|
'battery_level']
|
||||||
|
|
||||||
SENSOR_UNITS = {'humidity': '%', 'battery_level': '%'}
|
SENSOR_UNITS = {'humidity': '%', 'battery_level': 'V'}
|
||||||
|
|
||||||
SENSOR_TEMP_TYPES = ['temperature',
|
SENSOR_TEMP_TYPES = ['temperature',
|
||||||
'target',
|
'target',
|
||||||
|
@ -96,5 +96,7 @@ class OneWire(Entity):
|
|||||||
equals_pos = lines[1].find('t=')
|
equals_pos = lines[1].find('t=')
|
||||||
if equals_pos != -1:
|
if equals_pos != -1:
|
||||||
temp_string = lines[1][equals_pos+2:]
|
temp_string = lines[1][equals_pos+2:]
|
||||||
temp = float(temp_string) / 1000.0
|
temp = round(float(temp_string) / 1000.0, 1)
|
||||||
|
if temp < -55 or temp > 125:
|
||||||
|
return
|
||||||
self._state = temp
|
self._state = temp
|
||||||
|
@ -21,7 +21,9 @@ DATA_TYPES = OrderedDict([
|
|||||||
('Humidity', '%'),
|
('Humidity', '%'),
|
||||||
('Barometer', ''),
|
('Barometer', ''),
|
||||||
('Wind direction', ''),
|
('Wind direction', ''),
|
||||||
('Rain rate', '')])
|
('Rain rate', ''),
|
||||||
|
('Energy usage', 'W'),
|
||||||
|
('Total usage', 'W')])
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -87,7 +89,7 @@ class RfxtrxSensor(Entity):
|
|||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def device_state_attributes(self):
|
||||||
return self.event.values
|
return self.event.values
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
121
homeassistant/components/sensor/speedtest.py
Normal file
121
homeassistant/components/sensor/speedtest.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.sensor.speedtest
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Speedtest.net sensor based on speedtest-cli.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.speedtest/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
from datetime import timedelta
|
||||||
|
from subprocess import check_output
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.helpers.event import track_time_change
|
||||||
|
from homeassistant.components.sensor import DOMAIN
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
REQUIREMENTS = ['speedtest-cli==0.3.4']
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_SPEEDTEST_REGEX = re.compile(r'Ping:\s(\d+\.\d+)\sms[\r\n]+'
|
||||||
|
r'Download:\s(\d+\.\d+)\sMbit/s[\r\n]+'
|
||||||
|
r'Upload:\s(\d+\.\d+)\sMbit/s[\r\n]+')
|
||||||
|
|
||||||
|
CONF_MONITORED_CONDITIONS = 'monitored_conditions'
|
||||||
|
CONF_MINUTE = 'minute'
|
||||||
|
CONF_HOUR = 'hour'
|
||||||
|
CONF_DAY = 'day'
|
||||||
|
SENSOR_TYPES = {
|
||||||
|
'ping': ['Ping', 'ms'],
|
||||||
|
'download': ['Download', 'Mbit/s'],
|
||||||
|
'upload': ['Upload', 'Mbit/s'],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Return cached results if last scan was less then this time ago
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Setup the Speedtest sensor. """
|
||||||
|
|
||||||
|
data = SpeedtestData(hass, config)
|
||||||
|
dev = []
|
||||||
|
for sensor in config[CONF_MONITORED_CONDITIONS]:
|
||||||
|
if sensor not in SENSOR_TYPES:
|
||||||
|
_LOGGER.error('Sensor type: "%s" does not exist', sensor)
|
||||||
|
else:
|
||||||
|
dev.append(SpeedtestSensor(data, sensor))
|
||||||
|
|
||||||
|
add_devices(dev)
|
||||||
|
|
||||||
|
def update(call=None):
|
||||||
|
""" Update service for manual updates. """
|
||||||
|
data.update(dt_util.now())
|
||||||
|
for sensor in dev:
|
||||||
|
sensor.update()
|
||||||
|
|
||||||
|
hass.services.register(DOMAIN, 'update_speedtest', update)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
class SpeedtestSensor(Entity):
|
||||||
|
""" Implements a speedtest.net sensor. """
|
||||||
|
|
||||||
|
def __init__(self, speedtest_data, sensor_type):
|
||||||
|
self._name = SENSOR_TYPES[sensor_type][0]
|
||||||
|
self.speedtest_client = speedtest_data
|
||||||
|
self.type = sensor_type
|
||||||
|
self._state = None
|
||||||
|
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return '{} {}'.format('Speedtest', self._name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" Returns the state of the device. """
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
""" Unit of measurement of this entity, if any. """
|
||||||
|
return self._unit_of_measurement
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
""" Gets the latest data from Forecast.io and updates the states. """
|
||||||
|
data = self.speedtest_client.data
|
||||||
|
if data is not None:
|
||||||
|
if self.type == 'ping':
|
||||||
|
self._state = data['ping']
|
||||||
|
elif self.type == 'download':
|
||||||
|
self._state = data['download']
|
||||||
|
elif self.type == 'upload':
|
||||||
|
self._state = data['upload']
|
||||||
|
|
||||||
|
|
||||||
|
class SpeedtestData(object):
|
||||||
|
""" Gets the latest data from speedtest.net. """
|
||||||
|
|
||||||
|
def __init__(self, hass, config):
|
||||||
|
self.data = None
|
||||||
|
self.hass = hass
|
||||||
|
self.path = hass.config.path
|
||||||
|
track_time_change(self.hass, self.update,
|
||||||
|
minute=config.get(CONF_MINUTE, 0),
|
||||||
|
hour=config.get(CONF_HOUR, None),
|
||||||
|
day=config.get(CONF_DAY, None))
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
|
def update(self, now):
|
||||||
|
""" Gets the latest data from speedtest.net. """
|
||||||
|
_LOGGER.info('Executing speedtest')
|
||||||
|
re_output = _SPEEDTEST_REGEX.split(
|
||||||
|
check_output([sys.executable, self.path(
|
||||||
|
'lib', 'speedtest_cli.py'), '--simple']).decode("utf-8"))
|
||||||
|
self.data = {'ping': round(float(re_output[1]), 2),
|
||||||
|
'download': round(float(re_output[2]), 2),
|
||||||
|
'upload': round(float(re_output[3]), 2)}
|
@ -23,6 +23,7 @@ ATTR_DEPARTURE_TIME2 = 'Next on departure'
|
|||||||
ATTR_START = 'Start'
|
ATTR_START = 'Start'
|
||||||
ATTR_TARGET = 'Destination'
|
ATTR_TARGET = 'Destination'
|
||||||
ATTR_REMAINING_TIME = 'Remaining time'
|
ATTR_REMAINING_TIME = 'Remaining time'
|
||||||
|
ICON = 'mdi:bus'
|
||||||
|
|
||||||
# Return cached results if last scan was less then this time ago
|
# Return cached results if last scan was less then this time ago
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||||
@ -74,7 +75,7 @@ class SwissPublicTransportSensor(Entity):
|
|||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def device_state_attributes(self):
|
||||||
""" Returns the state attributes. """
|
""" Returns the state attributes. """
|
||||||
if self._times is not None:
|
if self._times is not None:
|
||||||
return {
|
return {
|
||||||
@ -86,6 +87,11 @@ class SwissPublicTransportSensor(Entity):
|
|||||||
':'.join(str(self._times[2]).split(':')[:2]))
|
':'.join(str(self._times[2]).split(':')[:2]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
""" Icon to use in the frontend, if any. """
|
||||||
|
return ICON
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
def update(self):
|
def update(self):
|
||||||
""" Gets the latest data from opendata.ch and updates the states. """
|
""" Gets the latest data from opendata.ch and updates the states. """
|
||||||
|
@ -67,10 +67,12 @@ class SystemMonitorSensor(Entity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
""" Returns the name of the sensor. """
|
||||||
return self._name.rstrip()
|
return self._name.rstrip()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
|
""" Icon to use in the frontend, if any. """
|
||||||
return SENSOR_TYPES[self.type][2]
|
return SENSOR_TYPES[self.type][2]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -80,10 +82,12 @@ class SystemMonitorSensor(Entity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
|
""" Unit of measurement of this entity, if any. """
|
||||||
return self._unit_of_measurement
|
return self._unit_of_measurement
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
def update(self):
|
def update(self):
|
||||||
|
""" Get the latest system informations. """
|
||||||
import psutil
|
import psutil
|
||||||
if self.type == 'disk_use_percent':
|
if self.type == 'disk_use_percent':
|
||||||
self._state = psutil.disk_usage(self.argument).percent
|
self._state = psutil.disk_usage(self.argument).percent
|
||||||
|
@ -11,7 +11,9 @@ import logging
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from homeassistant.const import TEMP_CELCIUS, ATTR_BATTERY_LEVEL
|
from homeassistant.const import (TEMP_CELCIUS,
|
||||||
|
ATTR_BATTERY_LEVEL,
|
||||||
|
DEVICE_DEFAULT_NAME)
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.components import tellduslive
|
from homeassistant.components import tellduslive
|
||||||
|
|
||||||
@ -44,50 +46,86 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
""" Sets up Tellstick sensors. """
|
""" Sets up Tellstick sensors. """
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
sensors = tellduslive.NETWORK.get_sensors()
|
add_devices(TelldusLiveSensor(sensor) for sensor in discovery_info)
|
||||||
devices = []
|
|
||||||
|
|
||||||
for component in sensors:
|
|
||||||
for sensor in component["data"]:
|
|
||||||
# one component can have more than one sensor
|
|
||||||
# (e.g. both humidity and temperature)
|
|
||||||
devices.append(TelldusLiveSensor(component["id"],
|
|
||||||
component["name"],
|
|
||||||
sensor["name"]))
|
|
||||||
add_devices(devices)
|
|
||||||
|
|
||||||
|
|
||||||
class TelldusLiveSensor(Entity):
|
class TelldusLiveSensor(Entity):
|
||||||
""" Represents a Telldus Live sensor. """
|
""" Represents a Telldus Live sensor. """
|
||||||
|
|
||||||
def __init__(self, sensor_id, sensor_name, sensor_type):
|
def __init__(self, sensor_id):
|
||||||
self._sensor_id = sensor_id
|
self._id = sensor_id
|
||||||
self._sensor_type = sensor_type
|
|
||||||
self._state = None
|
|
||||||
self._name = sensor_name + ' ' + SENSOR_TYPES[sensor_type][0]
|
|
||||||
self._last_update = None
|
|
||||||
self._battery_level = None
|
|
||||||
self.update()
|
self.update()
|
||||||
|
_LOGGER.debug("created sensor %s", self)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
""" update sensor values """
|
||||||
|
tellduslive.NETWORK.update_sensors()
|
||||||
|
self._sensor = tellduslive.NETWORK.get_sensor(self._id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _sensor_name(self):
|
||||||
|
return self._sensor["name"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _sensor_value(self):
|
||||||
|
return self._sensor["data"]["value"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _sensor_type(self):
|
||||||
|
return self._sensor["data"]["name"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _battery_level(self):
|
||||||
|
sensor_battery_level = self._sensor.get("battery")
|
||||||
|
return round(sensor_battery_level * 100 / 255) \
|
||||||
|
if sensor_battery_level else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _last_updated(self):
|
||||||
|
sensor_last_updated = self._sensor.get("lastUpdated")
|
||||||
|
return str(datetime.fromtimestamp(sensor_last_updated)) \
|
||||||
|
if sensor_last_updated else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _value_as_temperature(self):
|
||||||
|
return round(float(self._sensor_value), 1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _value_as_humidity(self):
|
||||||
|
return int(round(float(self._sensor_value)))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
""" Returns the name of the device. """
|
""" Returns the name of the device. """
|
||||||
return self._name
|
return "{} {}".format(self._sensor_name or DEVICE_DEFAULT_NAME,
|
||||||
|
self.quantity_name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
return not self._sensor.get("offline", False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
""" Returns the state of the device. """
|
""" Returns the state of the device. """
|
||||||
return self._state
|
if self._sensor_type == SENSOR_TYPE_TEMP:
|
||||||
|
return self._value_as_temperature
|
||||||
|
elif self._sensor_type == SENSOR_TYPE_HUMIDITY:
|
||||||
|
return self._value_as_humidity
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def device_state_attributes(self):
|
||||||
attrs = dict()
|
attrs = {}
|
||||||
if self._battery_level is not None:
|
if self._battery_level is not None:
|
||||||
attrs[ATTR_BATTERY_LEVEL] = self._battery_level
|
attrs[ATTR_BATTERY_LEVEL] = self._battery_level
|
||||||
if self._last_update is not None:
|
if self._last_updated is not None:
|
||||||
attrs[ATTR_LAST_UPDATED] = self._last_update
|
attrs[ATTR_LAST_UPDATED] = self._last_updated
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def quantity_name(self):
|
||||||
|
""" name of quantity """
|
||||||
|
return SENSOR_TYPES[self._sensor_type][0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
return SENSOR_TYPES[self._sensor_type][1]
|
return SENSOR_TYPES[self._sensor_type][1]
|
||||||
@ -95,18 +133,3 @@ class TelldusLiveSensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return SENSOR_TYPES[self._sensor_type][2]
|
return SENSOR_TYPES[self._sensor_type][2]
|
||||||
|
|
||||||
def update(self):
|
|
||||||
values = tellduslive.NETWORK.get_sensor_value(self._sensor_id,
|
|
||||||
self._sensor_type)
|
|
||||||
self._state, self._battery_level, self._last_update = values
|
|
||||||
|
|
||||||
self._state = float(self._state)
|
|
||||||
if self._sensor_type == SENSOR_TYPE_TEMP:
|
|
||||||
self._state = round(self._state, 1)
|
|
||||||
elif self._sensor_type == SENSOR_TYPE_HUMIDITY:
|
|
||||||
self._state = int(round(self._state))
|
|
||||||
|
|
||||||
self._battery_level = round(self._battery_level * 100 / 255) # percent
|
|
||||||
|
|
||||||
self._last_update = str(datetime.fromtimestamp(self._last_update))
|
|
||||||
|
@ -9,16 +9,20 @@ https://home-assistant.io/components/sensor.template/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity, generate_entity_id
|
||||||
from homeassistant.core import EVENT_STATE_CHANGED
|
from homeassistant.core import EVENT_STATE_CHANGED
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_FRIENDLY_NAME,
|
ATTR_FRIENDLY_NAME,
|
||||||
CONF_VALUE_TEMPLATE,
|
CONF_VALUE_TEMPLATE,
|
||||||
ATTR_UNIT_OF_MEASUREMENT)
|
ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
|
||||||
from homeassistant.util import template
|
from homeassistant.util import template, slugify
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import DOMAIN
|
||||||
|
|
||||||
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
CONF_SENSORS = 'sensors'
|
CONF_SENSORS = 'sensors'
|
||||||
STATE_ERROR = 'error'
|
STATE_ERROR = 'error'
|
||||||
@ -34,9 +38,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
for device, device_config in config[CONF_SENSORS].items():
|
for device, device_config in config[CONF_SENSORS].items():
|
||||||
|
|
||||||
|
if device != slugify(device):
|
||||||
|
_LOGGER.error("Found invalid key for sensor.template: %s. "
|
||||||
|
"Use %s instead", device, slugify(device))
|
||||||
|
continue
|
||||||
|
|
||||||
if not isinstance(device_config, dict):
|
if not isinstance(device_config, dict):
|
||||||
_LOGGER.error("Missing configuration data for sensor %s", device)
|
_LOGGER.error("Missing configuration data for sensor %s", device)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
||||||
unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT)
|
unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
state_template = device_config.get(CONF_VALUE_TEMPLATE)
|
state_template = device_config.get(CONF_VALUE_TEMPLATE)
|
||||||
@ -44,14 +55,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Missing %s for sensor %s", CONF_VALUE_TEMPLATE, device)
|
"Missing %s for sensor %s", CONF_VALUE_TEMPLATE, device)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sensors.append(
|
sensors.append(
|
||||||
SensorTemplate(
|
SensorTemplate(
|
||||||
hass,
|
hass,
|
||||||
|
device,
|
||||||
friendly_name,
|
friendly_name,
|
||||||
unit_of_measurement,
|
unit_of_measurement,
|
||||||
state_template)
|
state_template)
|
||||||
)
|
)
|
||||||
if sensors is None:
|
if not sensors:
|
||||||
_LOGGER.error("No sensors added")
|
_LOGGER.error("No sensors added")
|
||||||
return False
|
return False
|
||||||
add_devices(sensors)
|
add_devices(sensors)
|
||||||
@ -64,10 +77,15 @@ class SensorTemplate(Entity):
|
|||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
hass,
|
hass,
|
||||||
|
device_id,
|
||||||
friendly_name,
|
friendly_name,
|
||||||
unit_of_measurement,
|
unit_of_measurement,
|
||||||
state_template):
|
state_template):
|
||||||
|
|
||||||
|
self.entity_id = generate_entity_id(
|
||||||
|
ENTITY_ID_FORMAT, device_id,
|
||||||
|
hass=hass)
|
||||||
|
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._name = friendly_name
|
self._name = friendly_name
|
||||||
self._unit_of_measurement = unit_of_measurement
|
self._unit_of_measurement = unit_of_measurement
|
||||||
@ -76,10 +94,7 @@ class SensorTemplate(Entity):
|
|||||||
|
|
||||||
def _update_callback(_event):
|
def _update_callback(_event):
|
||||||
""" Called when the target device changes state. """
|
""" Called when the target device changes state. """
|
||||||
# This can be called before the entity is properly
|
self.update_ha_state(True)
|
||||||
# initialised, so check before updating state,
|
|
||||||
if self.entity_id:
|
|
||||||
self.update_ha_state(True)
|
|
||||||
|
|
||||||
self.hass.bus.listen(EVENT_STATE_CHANGED, _update_callback)
|
self.hass.bus.listen(EVENT_STATE_CHANGED, _update_callback)
|
||||||
|
|
||||||
@ -108,4 +123,9 @@ class SensorTemplate(Entity):
|
|||||||
self._state = template.render(self.hass, self._template)
|
self._state = template.render(self.hass, self._template)
|
||||||
except TemplateError as ex:
|
except TemplateError as ex:
|
||||||
self._state = STATE_ERROR
|
self._state = STATE_ERROR
|
||||||
|
if ex.args and ex.args[0].startswith(
|
||||||
|
"UndefinedError: 'None' has no attribute"):
|
||||||
|
# Common during HA startup - so just a warning
|
||||||
|
_LOGGER.warning(ex)
|
||||||
|
return
|
||||||
_LOGGER.error(ex)
|
_LOGGER.error(ex)
|
||||||
|
@ -59,6 +59,15 @@ class TimeDateSensor(Entity):
|
|||||||
""" Returns the state of the device. """
|
""" Returns the state of the device. """
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
if "date" in self.type and "time" in self.type:
|
||||||
|
return "mdi:calendar-clock"
|
||||||
|
elif "date" in self.type:
|
||||||
|
return "mdi:calendar"
|
||||||
|
else:
|
||||||
|
return "mdi:clock"
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
""" Gets the latest data and updates the states. """
|
""" Gets the latest data and updates the states. """
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ class TwitchSensor(Entity):
|
|||||||
self._state = STATE_OFFLINE
|
self._state = STATE_OFFLINE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def device_state_attributes(self):
|
||||||
""" Returns the state attributes. """
|
""" Returns the state attributes. """
|
||||||
if self._state == STATE_STREAMING:
|
if self._state == STATE_STREAMING:
|
||||||
return {
|
return {
|
||||||
@ -78,4 +78,5 @@ class TwitchSensor(Entity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
|
""" Icon to use in the frontend, if any. """
|
||||||
return ICON
|
return ICON
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||||||
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME,
|
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME,
|
||||||
TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP)
|
TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP)
|
||||||
|
|
||||||
REQUIREMENTS = ['pyvera==0.2.7']
|
REQUIREMENTS = ['pyvera==0.2.8']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ class VeraSensor(Entity):
|
|||||||
return '%'
|
return '%'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def device_state_attributes(self):
|
||||||
attr = {}
|
attr = {}
|
||||||
if self.vera_device.has_battery:
|
if self.vera_device.has_battery:
|
||||||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
||||||
|
@ -11,7 +11,7 @@ import logging
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED
|
from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED
|
||||||
|
|
||||||
REQUIREMENTS = ['python-wink==0.4.2']
|
REQUIREMENTS = ['python-wink==0.5.0']
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
@ -14,6 +14,7 @@ from homeassistant.helpers.entity import Entity
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DEFAULT_NAME = "Worldclock Sensor"
|
DEFAULT_NAME = "Worldclock Sensor"
|
||||||
|
ICON = 'mdi:clock'
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
@ -54,6 +55,11 @@ class WorldClockSensor(Entity):
|
|||||||
""" Returns the state of the device. """
|
""" Returns the state of the device. """
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
""" Icon to use in the frontend, if any. """
|
||||||
|
return ICON
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
""" Gets the time and updates the states. """
|
""" Gets the time and updates the states. """
|
||||||
self._state = dt_util.datetime_to_time_str(
|
self._state = dt_util.datetime_to_time_str(
|
||||||
|
@ -28,7 +28,7 @@ SENSOR_TYPES = {
|
|||||||
'temperature': ['Temperature', '°C'],
|
'temperature': ['Temperature', '°C'],
|
||||||
'windSpeed': ['Wind speed', 'm/s'],
|
'windSpeed': ['Wind speed', 'm/s'],
|
||||||
'windGust': ['Wind gust', 'm/s'],
|
'windGust': ['Wind gust', 'm/s'],
|
||||||
'pressure': ['Pressure', 'mbar'],
|
'pressure': ['Pressure', 'hPa'],
|
||||||
'windDirection': ['Wind direction', '°'],
|
'windDirection': ['Wind direction', '°'],
|
||||||
'humidity': ['Humidity', '%'],
|
'humidity': ['Humidity', '%'],
|
||||||
'fog': ['Fog', '%'],
|
'fog': ['Fog', '%'],
|
||||||
@ -100,7 +100,7 @@ class YrSensor(Entity):
|
|||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def device_state_attributes(self):
|
||||||
""" Returns state attributes. """
|
""" Returns state attributes. """
|
||||||
data = {
|
data = {
|
||||||
'about': "Weather forecast from yr.no, delivered by the"
|
'about': "Weather forecast from yr.no, delivered by the"
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
homeassistant.components.sensor.zigbee
|
homeassistant.components.sensor.zigbee
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Contains functionality to use a ZigBee device as a sensor.
|
Contains functionality to use a ZigBee device as a sensor.
|
||||||
"""
|
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.zigbee/
|
||||||
|
|
||||||
|
"""
|
||||||
import logging
|
import logging
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
|
|
||||||
@ -36,9 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
|
|
||||||
|
|
||||||
class ZigBeeTemperatureSensor(Entity):
|
class ZigBeeTemperatureSensor(Entity):
|
||||||
"""
|
""" Allows usage of an XBee Pro as a temperature sensor. """
|
||||||
Allows usage of an XBee Pro as a temperature sensor.
|
|
||||||
"""
|
|
||||||
def __init__(self, hass, config):
|
def __init__(self, hass, config):
|
||||||
self._config = config
|
self._config = config
|
||||||
self._temp = None
|
self._temp = None
|
||||||
|
@ -22,14 +22,24 @@ from homeassistant.components.zwave import (
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ON, STATE_OFF, TEMP_CELCIUS, TEMP_FAHRENHEIT)
|
STATE_ON, STATE_OFF, TEMP_CELCIUS, TEMP_FAHRENHEIT)
|
||||||
|
|
||||||
PHILIO = '013c'
|
PHILIO = '0x013c'
|
||||||
PHILIO_SLIM_SENSOR = '0002'
|
PHILIO_SLIM_SENSOR = '0x0002'
|
||||||
PHILIO_SLIM_SENSOR_MOTION = (PHILIO, PHILIO_SLIM_SENSOR, 0)
|
PHILIO_SLIM_SENSOR_MOTION = (PHILIO, PHILIO_SLIM_SENSOR, 0)
|
||||||
|
|
||||||
|
FIBARO = '0x010f'
|
||||||
|
FIBARO_WALL_PLUG = '0x1000'
|
||||||
|
FIBARO_WALL_PLUG_SENSOR_METER = (FIBARO, FIBARO_WALL_PLUG, 8)
|
||||||
|
|
||||||
WORKAROUND_NO_OFF_EVENT = 'trigger_no_off_event'
|
WORKAROUND_NO_OFF_EVENT = 'trigger_no_off_event'
|
||||||
|
WORKAROUND_IGNORE = 'ignore'
|
||||||
|
|
||||||
DEVICE_MAPPINGS = {
|
DEVICE_MAPPINGS = {
|
||||||
PHILIO_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT,
|
PHILIO_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT,
|
||||||
|
|
||||||
|
# For some reason Fibaro Wall Plug reports 2 power consumptions.
|
||||||
|
# One value updates as the power consumption changes
|
||||||
|
# and the other does not change.
|
||||||
|
FIBARO_WALL_PLUG_SENSOR_METER: WORKAROUND_IGNORE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -66,6 +76,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
add_devices([
|
add_devices([
|
||||||
ZWaveTriggerSensor(value, hass, re_arm_multiplier * 8)
|
ZWaveTriggerSensor(value, hass, re_arm_multiplier * 8)
|
||||||
])
|
])
|
||||||
|
elif DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_IGNORE:
|
||||||
|
return
|
||||||
|
|
||||||
# generic Device mappings
|
# generic Device mappings
|
||||||
elif value.command_class == COMMAND_CLASS_SENSOR_BINARY:
|
elif value.command_class == COMMAND_CLASS_SENSOR_BINARY:
|
||||||
|
90
homeassistant/components/splunk.py
Normal file
90
homeassistant/components/splunk.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import homeassistant.util as util
|
||||||
|
from homeassistant.helpers import validate_config
|
||||||
|
from homeassistant.helpers import state as state_helper
|
||||||
|
from homeassistant.const import EVENT_STATE_CHANGED
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DOMAIN = "splunk"
|
||||||
|
DEPENDENCIES = []
|
||||||
|
|
||||||
|
DEFAULT_HOST = 'localhost'
|
||||||
|
DEFAULT_PORT = '8088'
|
||||||
|
DEFAULT_SSL = False
|
||||||
|
|
||||||
|
CONF_HOST = 'host'
|
||||||
|
CONF_PORT = 'port'
|
||||||
|
CONF_TOKEN = 'token'
|
||||||
|
CONF_SSL = 'SSL'
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
""" Setup the Splunk component. """
|
||||||
|
|
||||||
|
if not validate_config(config, {DOMAIN: ['token']}, _LOGGER):
|
||||||
|
_LOGGER.error("You must include the token for your HTTP "
|
||||||
|
"Event Collector input in Splunk.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
conf = config[DOMAIN]
|
||||||
|
|
||||||
|
host = conf[CONF_HOST]
|
||||||
|
port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT)
|
||||||
|
token = util.convert(conf.get(CONF_TOKEN), str)
|
||||||
|
use_ssl = util.convert(conf.get(CONF_SSL), bool, DEFAULT_SSL)
|
||||||
|
if use_ssl:
|
||||||
|
uri_scheme = "https://"
|
||||||
|
else:
|
||||||
|
uri_scheme = "http://"
|
||||||
|
event_collector = uri_scheme + host + ":" + str(port) + \
|
||||||
|
"/services/collector/event"
|
||||||
|
headers = {'Authorization': 'Splunk ' + token}
|
||||||
|
|
||||||
|
def splunk_event_listener(event):
|
||||||
|
""" Listen for new messages on the bus and sends them to Splunk. """
|
||||||
|
|
||||||
|
state = event.data.get('new_state')
|
||||||
|
|
||||||
|
if state is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
_state = state_helper.state_as_number(state)
|
||||||
|
except ValueError:
|
||||||
|
_state = state.state
|
||||||
|
|
||||||
|
json_body = [
|
||||||
|
{
|
||||||
|
'domain': state.domain,
|
||||||
|
'entity_id': state.object_id,
|
||||||
|
'attributes': state.attributes,
|
||||||
|
'time': str(event.time_fired),
|
||||||
|
'value': _state,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = {"host": event_collector,
|
||||||
|
"event": json_body}
|
||||||
|
requests.post(event_collector, data=json.dumps(payload),
|
||||||
|
headers=headers)
|
||||||
|
except requests.exceptions.RequestException as error:
|
||||||
|
_LOGGER.exception('Error saving event to Splunk: %s', error)
|
||||||
|
|
||||||
|
hass.bus.listen(EVENT_STATE_CHANGED, splunk_event_listener)
|
||||||
|
|
||||||
|
return True
|
@ -8,10 +8,8 @@ https://home-assistant.io/components/statsd/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.const import (EVENT_STATE_CHANGED, STATE_ON, STATE_OFF,
|
from homeassistant.const import EVENT_STATE_CHANGED
|
||||||
STATE_UNLOCKED, STATE_LOCKED, STATE_UNKNOWN)
|
from homeassistant.helpers import state as state_helper
|
||||||
from homeassistant.components.sun import (STATE_ABOVE_HORIZON,
|
|
||||||
STATE_BELOW_HORIZON)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -61,19 +59,10 @@ def setup(hass, config):
|
|||||||
if state is None:
|
if state is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON):
|
try:
|
||||||
_state = 1
|
_state = state_helper.state_as_number(state)
|
||||||
elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_UNKNOWN,
|
except ValueError:
|
||||||
STATE_BELOW_HORIZON):
|
return
|
||||||
_state = 0
|
|
||||||
else:
|
|
||||||
_state = state.state
|
|
||||||
if _state == '':
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
_state = float(_state)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not isinstance(_state, NUM_TYPES):
|
if not isinstance(_state, NUM_TYPES):
|
||||||
return
|
return
|
||||||
|
@ -129,11 +129,6 @@ class SwitchDevice(ToggleEntity):
|
|||||||
""" Is the device in standby. """
|
""" Is the device in standby. """
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
|
||||||
def device_state_attributes(self):
|
|
||||||
""" Returns device specific state attributes. """
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
""" Returns optional state attributes. """
|
""" Returns optional state attributes. """
|
||||||
@ -144,9 +139,4 @@ class SwitchDevice(ToggleEntity):
|
|||||||
if value:
|
if value:
|
||||||
data[attr] = value
|
data[attr] = value
|
||||||
|
|
||||||
device_attr = self.device_state_attributes
|
|
||||||
|
|
||||||
if device_attr is not None:
|
|
||||||
data.update(device_attr)
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
103
homeassistant/components/switch/mfi.py
Normal file
103
homeassistant/components/switch/mfi.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.switch.mfi
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Support for Ubiquiti mFi switches.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/switch.mfi/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.switch import DOMAIN, SwitchDevice
|
||||||
|
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||||
|
from homeassistant.helpers import validate_config
|
||||||
|
|
||||||
|
REQUIREMENTS = ['mficlient==0.2.2']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SWITCH_MODELS = [
|
||||||
|
'Outlet',
|
||||||
|
'Output 5v',
|
||||||
|
'Output 12v',
|
||||||
|
'Output 24v',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-variable
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Sets up mFi sensors. """
|
||||||
|
|
||||||
|
if not validate_config({DOMAIN: config},
|
||||||
|
{DOMAIN: ['host',
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_PASSWORD]},
|
||||||
|
_LOGGER):
|
||||||
|
_LOGGER.error('A host, username, and password are required')
|
||||||
|
return False
|
||||||
|
|
||||||
|
host = config.get('host')
|
||||||
|
port = int(config.get('port', 6443))
|
||||||
|
username = config.get('username')
|
||||||
|
password = config.get('password')
|
||||||
|
|
||||||
|
from mficlient.client import MFiClient
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = MFiClient(host, username, password, port=port)
|
||||||
|
except client.FailedToLogin as ex:
|
||||||
|
_LOGGER.error('Unable to connect to mFi: %s', str(ex))
|
||||||
|
return False
|
||||||
|
|
||||||
|
add_devices(MfiSwitch(port)
|
||||||
|
for device in client.get_devices()
|
||||||
|
for port in device.ports.values()
|
||||||
|
if port.model in SWITCH_MODELS)
|
||||||
|
|
||||||
|
|
||||||
|
class MfiSwitch(SwitchDevice):
|
||||||
|
""" An mFi switch-able device. """
|
||||||
|
def __init__(self, port):
|
||||||
|
self._port = port
|
||||||
|
self._target_state = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
return self._port.ident
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self._port.label
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
return self._port.output
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self._port.refresh()
|
||||||
|
if self._target_state is not None:
|
||||||
|
self._port.data['output'] = float(self._target_state)
|
||||||
|
self._target_state = None
|
||||||
|
|
||||||
|
def turn_on(self):
|
||||||
|
self._port.control(True)
|
||||||
|
self._target_state = True
|
||||||
|
|
||||||
|
def turn_off(self):
|
||||||
|
self._port.control(False)
|
||||||
|
self._target_state = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_power_mwh(self):
|
||||||
|
return int(self._port.data.get('active_pwr', 0) * 1000)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
attr = {}
|
||||||
|
attr['volts'] = round(self._port.data.get('v_rms', 0), 1)
|
||||||
|
attr['amps'] = round(self._port.data.get('i_rms', 0), 1)
|
||||||
|
return attr
|
@ -1,5 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
homeassistant.components.switch.mysensors
|
homeassistant.components.switch.mysensors.
|
||||||
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Support for MySensors switches.
|
Support for MySensors switches.
|
||||||
|
|
||||||
@ -29,14 +30,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
for gateway in mysensors.GATEWAYS.values():
|
for gateway in mysensors.GATEWAYS.values():
|
||||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||||
# states. Map them in a defaultdict(list).
|
# states. Map them in a dict of lists.
|
||||||
pres = gateway.const.Presentation
|
pres = gateway.const.Presentation
|
||||||
set_req = gateway.const.SetReq
|
set_req = gateway.const.SetReq
|
||||||
map_sv_types = {
|
map_sv_types = {
|
||||||
pres.S_DOOR: [set_req.V_ARMED],
|
pres.S_DOOR: [set_req.V_ARMED],
|
||||||
pres.S_MOTION: [set_req.V_ARMED],
|
pres.S_MOTION: [set_req.V_ARMED],
|
||||||
pres.S_SMOKE: [set_req.V_ARMED],
|
pres.S_SMOKE: [set_req.V_ARMED],
|
||||||
pres.S_LIGHT: [set_req.V_LIGHT],
|
|
||||||
pres.S_LOCK: [set_req.V_LOCK_STATUS],
|
pres.S_LOCK: [set_req.V_LOCK_STATUS],
|
||||||
}
|
}
|
||||||
if float(gateway.version) >= 1.5:
|
if float(gateway.version) >= 1.5:
|
||||||
@ -48,7 +48,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
pres.S_VIBRATION: [set_req.V_ARMED],
|
pres.S_VIBRATION: [set_req.V_ARMED],
|
||||||
pres.S_MOISTURE: [set_req.V_ARMED],
|
pres.S_MOISTURE: [set_req.V_ARMED],
|
||||||
})
|
})
|
||||||
map_sv_types[pres.S_LIGHT].append(set_req.V_STATUS)
|
|
||||||
|
|
||||||
devices = {}
|
devices = {}
|
||||||
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||||
@ -100,28 +99,24 @@ class MySensorsSwitch(SwitchDevice):
|
|||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return device specific state attributes."""
|
"""Return device specific state attributes."""
|
||||||
device_attr = {}
|
attr = {
|
||||||
for value_type, value in self._values.items():
|
|
||||||
if value_type != self.value_type:
|
|
||||||
device_attr[self.gateway.const.SetReq(value_type).name] = value
|
|
||||||
return device_attr
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state_attributes(self):
|
|
||||||
"""Return the state attributes."""
|
|
||||||
data = {
|
|
||||||
mysensors.ATTR_PORT: self.gateway.port,
|
mysensors.ATTR_PORT: self.gateway.port,
|
||||||
mysensors.ATTR_NODE_ID: self.node_id,
|
mysensors.ATTR_NODE_ID: self.node_id,
|
||||||
mysensors.ATTR_CHILD_ID: self.child_id,
|
mysensors.ATTR_CHILD_ID: self.child_id,
|
||||||
ATTR_BATTERY_LEVEL: self.battery_level,
|
ATTR_BATTERY_LEVEL: self.battery_level,
|
||||||
}
|
}
|
||||||
|
|
||||||
device_attr = self.device_state_attributes
|
set_req = self.gateway.const.SetReq
|
||||||
|
|
||||||
if device_attr is not None:
|
for value_type, value in self._values.items():
|
||||||
data.update(device_attr)
|
if value_type != self.value_type:
|
||||||
|
try:
|
||||||
return data
|
attr[set_req(value_type).name] = value
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.error('value_type %s is not valid for mysensors '
|
||||||
|
'version %s', value_type,
|
||||||
|
self.gateway.version)
|
||||||
|
return attr
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
@ -134,25 +129,33 @@ class MySensorsSwitch(SwitchDevice):
|
|||||||
"""Turn the switch on."""
|
"""Turn the switch on."""
|
||||||
self.gateway.set_child_value(
|
self.gateway.set_child_value(
|
||||||
self.node_id, self.child_id, self.value_type, 1)
|
self.node_id, self.child_id, self.value_type, 1)
|
||||||
self._values[self.value_type] = STATE_ON
|
if self.gateway.optimistic:
|
||||||
self.update_ha_state()
|
# optimistically assume that switch has changed state
|
||||||
|
self._values[self.value_type] = STATE_ON
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
"""Turn the switch off."""
|
"""Turn the switch off."""
|
||||||
self.gateway.set_child_value(
|
self.gateway.set_child_value(
|
||||||
self.node_id, self.child_id, self.value_type, 0)
|
self.node_id, self.child_id, self.value_type, 0)
|
||||||
self._values[self.value_type] = STATE_OFF
|
if self.gateway.optimistic:
|
||||||
self.update_ha_state()
|
# optimistically assume that switch has changed state
|
||||||
|
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):
|
def update(self):
|
||||||
"""Update the controller with the latest value from a sensor."""
|
"""Update the controller with the latest value from a sensor."""
|
||||||
node = self.gateway.sensors[self.node_id]
|
node = self.gateway.sensors[self.node_id]
|
||||||
child = node.children[self.child_id]
|
child = node.children[self.child_id]
|
||||||
for value_type, value in child.values.items():
|
for value_type, value in child.values.items():
|
||||||
_LOGGER.info(
|
_LOGGER.debug(
|
||||||
"%s: value_type %s, value = %s", self._name, value_type, value)
|
"%s: value_type %s, value = %s", self._name, value_type, value)
|
||||||
if value_type == self.gateway.const.SetReq.V_ARMED or \
|
if value_type == self.gateway.const.SetReq.V_ARMED or \
|
||||||
value_type == self.gateway.const.SetReq.V_STATUS or \
|
|
||||||
value_type == self.gateway.const.SetReq.V_LIGHT or \
|
value_type == self.gateway.const.SetReq.V_LIGHT or \
|
||||||
value_type == self.gateway.const.SetReq.V_LOCK_STATUS:
|
value_type == self.gateway.const.SetReq.V_LOCK_STATUS:
|
||||||
self._values[value_type] = (
|
self._values[value_type] = (
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user