commit
853a9fd4cd
36 changed files with 924 additions and 297 deletions
|
@ -26,6 +26,9 @@ omit =
|
|||
homeassistant/components/zwave.py
|
||||
homeassistant/components/*/zwave.py
|
||||
|
||||
homeassistant/components/rfxtrx.py
|
||||
homeassistant/components/*/rfxtrx.py
|
||||
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/*
|
||||
|
@ -39,11 +42,13 @@ omit =
|
|||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
|
@ -60,6 +65,7 @@ omit =
|
|||
homeassistant/components/notify/slack.py
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
|
@ -71,7 +77,6 @@ omit =
|
|||
homeassistant/components/sensor/mysensors.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/rfxtrx.py
|
||||
homeassistant/components/sensor/rpi_gpio.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
recursive-exclude tests *
|
||||
recursive-include homeassistant services.yaml
|
||||
include README.md
|
||||
include LICENSE
|
||||
graft homeassistant
|
||||
prune homeassistant/components/frontend/www_static/home-assistant-polymer
|
||||
recursive-exclude * *.py[co]
|
||||
|
|
12
README.md
12
README.md
|
@ -16,10 +16,12 @@ Check out [the website](https://home-assistant.io) for [a demo][demo], installat
|
|||
|
||||
Examples of devices it can interface it:
|
||||
|
||||
* Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), and [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/)
|
||||
* Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/) and any SNMP capable Linksys WAP/WRT
|
||||
*
|
||||
* [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, [Edimax](http://www.edimax.com/) switches, [Efergy](https://efergy.com) energy monitoring, RFXtrx sensors, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors
|
||||
* [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), [Kodi (XBMC)](http://kodi.tv/), and iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api))
|
||||
* Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/)
|
||||
* [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), [Plex](https://plex.tv/), [Kodi (XBMC)](http://kodi.tv/), and iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api))
|
||||
* Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), [RFXtrx](http://www.rfxcom.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/)
|
||||
* Interaction with [IFTTT](https://ifttt.com/)
|
||||
* Integrate data from the [Bitcoin](https://bitcoin.org) network, meteorological data from [OpenWeatherMap](http://openweathermap.org/) and [Forecast.io](https://forecast.io/), [Transmission](http://www.transmissionbt.com/), or [SABnzbd](http://sabnzbd.org).
|
||||
* [See full list of supported devices](https://home-assistant.io/components/)
|
||||
|
||||
|
@ -29,8 +31,8 @@ Built home automation on top of your devices:
|
|||
* Turn on the lights when people get home after sun set
|
||||
* Turn on lights slowly during sun set to compensate for less light
|
||||
* Turn off all lights and devices when everybody leaves the house
|
||||
* Offers a [REST API](https://home-assistant.io/developers/api.html) and can interface with MQTT for easy integration with other projects
|
||||
* Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), [Slack](https://slack.com/), and [Jabber (XMPP)](http://xmpp.org)
|
||||
* Offers a [REST API](https://home-assistant.io/developers/api.html) and can interface with MQTT for easy integration with other projects like [OwnTracks](http://owntracks.org/)
|
||||
* Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), [Slack](https://slack.com/), [Telegram](https://telegram.org/), and [Jabber (XMPP)](http://xmpp.org)
|
||||
|
||||
The system is built modular so support for other devices or actions can be implemented easily. See also the [section on architecture](https://home-assistant.io/developers/architecture.html) and the [section on creating your own components](https://home-assistant.io/developers/creating_components.html).
|
||||
|
||||
|
|
|
@ -134,6 +134,9 @@ automation:
|
|||
service: light.turn_off
|
||||
entity_id: group.all_lights
|
||||
|
||||
# Sensors need to be added into the configuration.yaml as sensor:, sensor 2:, sensor 3:, etc.
|
||||
# Each sensor label should be unique or your sensors might not load correctly.
|
||||
|
||||
sensor:
|
||||
platform: systemmonitor
|
||||
resources:
|
||||
|
|
|
@ -114,6 +114,8 @@ def setup(hass, config):
|
|||
os.remove(csv_path)
|
||||
|
||||
conf = config.get(DOMAIN, {})
|
||||
if isinstance(conf, list):
|
||||
conf = conf[0]
|
||||
consider_home = timedelta(
|
||||
seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int,
|
||||
DEFAULT_CONSIDER_HOME))
|
||||
|
|
|
@ -46,6 +46,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}')
|
||||
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
@ -77,7 +78,7 @@ class DdWrtDeviceScanner(object):
|
|||
|
||||
self.last_results = {}
|
||||
|
||||
self.mac2name = None
|
||||
self.mac2name = {}
|
||||
|
||||
# Test the router is accessible
|
||||
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
|
||||
|
@ -98,30 +99,33 @@ class DdWrtDeviceScanner(object):
|
|||
|
||||
with self.lock:
|
||||
# if not initialised and not already scanned and not found
|
||||
if self.mac2name is None or device not in self.mac2name:
|
||||
if device not in self.mac2name:
|
||||
url = 'http://{}/Status_Lan.live.asp'.format(self.host)
|
||||
data = self.get_ddwrt_data(url)
|
||||
|
||||
if not data:
|
||||
return
|
||||
return None
|
||||
|
||||
dhcp_leases = data.get('dhcp_leases', None)
|
||||
if dhcp_leases:
|
||||
# remove leading and trailing single quotes
|
||||
cleaned_str = dhcp_leases.strip().strip('"')
|
||||
elements = cleaned_str.split('","')
|
||||
num_clients = int(len(elements)/5)
|
||||
self.mac2name = {}
|
||||
for idx in range(0, num_clients):
|
||||
# this is stupid but the data is a single array
|
||||
# every 5 elements represents one hosts, the MAC
|
||||
# is the third element and the name is the first
|
||||
mac_index = (idx * 5) + 2
|
||||
if mac_index < len(elements):
|
||||
mac = elements[mac_index]
|
||||
self.mac2name[mac] = elements[idx * 5]
|
||||
|
||||
return self.mac2name.get(device, None)
|
||||
if not dhcp_leases:
|
||||
return None
|
||||
|
||||
# remove leading and trailing single quotes
|
||||
cleaned_str = dhcp_leases.strip().strip('"')
|
||||
elements = cleaned_str.split('","')
|
||||
num_clients = int(len(elements)/5)
|
||||
self.mac2name = {}
|
||||
for idx in range(0, num_clients):
|
||||
# this is stupid but the data is a single array
|
||||
# every 5 elements represents one hosts, the MAC
|
||||
# is the third element and the name is the first
|
||||
mac_index = (idx * 5) + 2
|
||||
if mac_index < len(elements):
|
||||
mac = elements[mac_index]
|
||||
self.mac2name[mac] = elements[idx * 5]
|
||||
|
||||
return self.mac2name.get(device)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
|
@ -141,29 +145,25 @@ class DdWrtDeviceScanner(object):
|
|||
if not data:
|
||||
return False
|
||||
|
||||
if data:
|
||||
self.last_results = []
|
||||
active_clients = data.get('active_wireless', None)
|
||||
if active_clients:
|
||||
# This is really lame, instead of using JSON the DD-WRT UI
|
||||
# uses its own data format for some reason and then
|
||||
# regex's out values so I guess I have to do the same,
|
||||
# LAME!!!
|
||||
self.last_results = []
|
||||
|
||||
# remove leading and trailing single quotes
|
||||
clean_str = active_clients.strip().strip("'")
|
||||
elements = clean_str.split("','")
|
||||
active_clients = data.get('active_wireless', None)
|
||||
if not active_clients:
|
||||
return False
|
||||
|
||||
num_clients = int(len(elements)/9)
|
||||
for idx in range(0, num_clients):
|
||||
# get every 9th element which is the MAC address
|
||||
index = idx * 9
|
||||
if index < len(elements):
|
||||
self.last_results.append(elements[index])
|
||||
# This is really lame, instead of using JSON the DD-WRT UI
|
||||
# uses its own data format for some reason and then
|
||||
# regex's out values so I guess I have to do the same,
|
||||
# LAME!!!
|
||||
|
||||
return True
|
||||
# remove leading and trailing single quotes
|
||||
clean_str = active_clients.strip().strip("'")
|
||||
elements = clean_str.split("','")
|
||||
|
||||
return False
|
||||
self.last_results.extend(item for item in elements
|
||||
if _MAC_REGEX.match(item))
|
||||
|
||||
return True
|
||||
|
||||
def get_ddwrt_data(self, url):
|
||||
""" Retrieve data from DD-WRT and return parsed result. """
|
||||
|
|
|
@ -117,15 +117,18 @@ class NmapDeviceScanner(object):
|
|||
scanner = PortScanner()
|
||||
|
||||
options = "-F --host-timeout 5"
|
||||
exclude_targets = set()
|
||||
|
||||
if self.home_interval:
|
||||
now = dt_util.now()
|
||||
for host in self.last_results:
|
||||
if host.last_update + self.home_interval > now:
|
||||
exclude_targets.add(host)
|
||||
if len(exclude_targets) > 0:
|
||||
target_list = [t.ip for t in exclude_targets]
|
||||
options += " --exclude {}".format(",".join(target_list))
|
||||
boundary = dt_util.now() - self.home_interval
|
||||
last_results = [device for device in self.last_results
|
||||
if device.last_update > boundary]
|
||||
if last_results:
|
||||
# Pylint is confused here.
|
||||
# pylint: disable=no-member
|
||||
options += " --exclude {}".format(",".join(device.ip for device
|
||||
in last_results))
|
||||
else:
|
||||
last_results = []
|
||||
|
||||
try:
|
||||
result = scanner.scan(hosts=self.hosts, arguments=options)
|
||||
|
@ -133,18 +136,17 @@ class NmapDeviceScanner(object):
|
|||
return False
|
||||
|
||||
now = dt_util.now()
|
||||
self.last_results = []
|
||||
for ipv4, info in result['scan'].items():
|
||||
if info['status']['state'] != 'up':
|
||||
continue
|
||||
name = info['hostnames'][0] if info['hostnames'] else ipv4
|
||||
name = info['hostnames'][0]['name'] if info['hostnames'] else ipv4
|
||||
# Mac address only returned if nmap ran as root
|
||||
mac = info['addresses'].get('mac') or _arp(ipv4)
|
||||
if mac is None:
|
||||
continue
|
||||
device = Device(mac.upper(), name, ipv4, now)
|
||||
self.last_results.append(device)
|
||||
self.last_results.extend(exclude_targets)
|
||||
last_results.append(Device(mac.upper(), name, ipv4, now))
|
||||
|
||||
self.last_results = last_results
|
||||
|
||||
_LOGGER.info("nmap scan successful")
|
||||
return True
|
||||
|
|
119
homeassistant/components/device_tracker/snmp.py
Normal file
119
homeassistant/components/device_tracker/snmp.py
Normal file
|
@ -0,0 +1,119 @@
|
|||
"""
|
||||
homeassistant.components.device_tracker.snmp
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports fetching WiFi associations
|
||||
through SNMP.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.snmp.html
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import threading
|
||||
import binascii
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pysnmp==4.2.5']
|
||||
|
||||
CONF_COMMUNITY = "community"
|
||||
CONF_BASEOID = "baseoid"
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns an snmp scanner """
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_COMMUNITY, CONF_BASEOID]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
scanner = SnmpScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class SnmpScanner(object):
|
||||
"""
|
||||
This class queries any SNMP capable Acces Point for connected devices.
|
||||
"""
|
||||
def __init__(self, config):
|
||||
self.host = config[CONF_HOST]
|
||||
self.community = config[CONF_COMMUNITY]
|
||||
self.baseoid = config[CONF_BASEOID]
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = []
|
||||
|
||||
# Test the router is accessible
|
||||
data = self.get_snmp_data()
|
||||
self.success_init = data is not None
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device IDs.
|
||||
"""
|
||||
|
||||
self._update_info()
|
||||
return [client['mac'] for client in self.last_results]
|
||||
|
||||
# Supressing no-self-use warning
|
||||
# pylint: disable=R0201
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
# We have no names
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensures the information from the WAP is up to date.
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
data = self.get_snmp_data()
|
||||
if not data:
|
||||
return False
|
||||
|
||||
self.last_results = data
|
||||
return True
|
||||
|
||||
def get_snmp_data(self):
|
||||
""" Fetch mac addresses from WAP via SNMP. """
|
||||
from pysnmp.entity.rfc3413.oneliner import cmdgen
|
||||
|
||||
devices = []
|
||||
|
||||
snmp = cmdgen.CommandGenerator()
|
||||
errindication, errstatus, errindex, restable = snmp.nextCmd(
|
||||
cmdgen.CommunityData(self.community),
|
||||
cmdgen.UdpTransportTarget((self.host, 161)),
|
||||
cmdgen.MibVariable(self.baseoid)
|
||||
)
|
||||
|
||||
if errindication:
|
||||
_LOGGER.error("SNMPLIB error: %s", errindication)
|
||||
return
|
||||
if errstatus:
|
||||
_LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(),
|
||||
errindex and restable[-1][int(errindex)-1]
|
||||
or '?')
|
||||
return
|
||||
|
||||
for resrow in restable:
|
||||
for _, val in resrow:
|
||||
mac = binascii.hexlify(val.asOctets()).decode('utf-8')
|
||||
mac = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)])
|
||||
devices.append({'mac': mac})
|
||||
return devices
|
|
@ -11,6 +11,7 @@ import logging
|
|||
from . import version
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import URL_ROOT, HTTP_OK
|
||||
from homeassistant.config import get_default_config_dir
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api']
|
||||
|
@ -19,7 +20,6 @@ INDEX_PATH = os.path.join(os.path.dirname(__file__), 'index.html.template')
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
FRONTEND_URLS = [
|
||||
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
|
||||
'/devEvent']
|
||||
|
@ -44,6 +44,9 @@ def setup(hass, config):
|
|||
hass.http.register_path(
|
||||
'HEAD', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_static, False)
|
||||
hass.http.register_path(
|
||||
'GET', re.compile(r'/local/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_local, False)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -84,3 +87,16 @@ def _handle_get_static(handler, path_match, data):
|
|||
path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
|
||||
|
||||
handler.write_file(path)
|
||||
|
||||
|
||||
def _handle_get_local(handler, path_match, data):
|
||||
"""
|
||||
Returns a static file from the hass.config.path/www for the frontend.
|
||||
"""
|
||||
req_file = util.sanitize_path(path_match.group('file'))
|
||||
|
||||
path = os.path.join(get_default_config_dir(), 'www', req_file)
|
||||
if not os.path.isfile(path):
|
||||
return False
|
||||
|
||||
handler.write_file(path)
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit c8d99bc3ea21cdd7bfb39e7700f92ed09f4b9efd
|
||||
Subproject commit 6989009b2d59e39fd39b3025ff5899877f618bd3
|
|
@ -246,6 +246,7 @@ def setup(hass, config):
|
|||
rgb_color = dat.get(ATTR_RGB_COLOR)
|
||||
|
||||
if len(rgb_color) == 3:
|
||||
params[ATTR_RGB_COLOR] = [int(val) for val in rgb_color]
|
||||
params[ATTR_XY_COLOR] = \
|
||||
color_util.color_RGB_to_xy(int(rgb_color[0]),
|
||||
int(rgb_color[1]),
|
||||
|
|
76
homeassistant/components/light/blinksticklight.py
Normal file
76
homeassistant/components/light/blinksticklight.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
"""
|
||||
homeassistant.components.light.blinksticklight
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Blinkstick lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.blinksticklight.html
|
||||
"""
|
||||
import logging
|
||||
|
||||
from blinkstick import blinkstick
|
||||
|
||||
from homeassistant.components.light import (Light, ATTR_RGB_COLOR)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REQUIREMENTS = ["blinkstick==1.1.7"]
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Add device specified by serial number. """
|
||||
stick = blinkstick.find_by_serial(config['serial'])
|
||||
|
||||
add_devices_callback([BlinkStickLight(stick, config['name'])])
|
||||
|
||||
|
||||
class BlinkStickLight(Light):
|
||||
""" Represents a BlinkStick light. """
|
||||
|
||||
def __init__(self, stick, name):
|
||||
self._stick = stick
|
||||
self._name = name
|
||||
self._serial = stick.get_serial()
|
||||
self._rgb_color = stick.get_color()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Polling needed. """
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the light. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
""" Read back the color of the light. """
|
||||
return self._rgb_color
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" Check whether any of the LEDs colors are non-zero. """
|
||||
return sum(self._rgb_color) > 0
|
||||
|
||||
def update(self):
|
||||
""" Read back the device state """
|
||||
self._rgb_color = self._stick.get_color()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
self._rgb_color = kwargs[ATTR_RGB_COLOR]
|
||||
else:
|
||||
self._rgb_color = [255, 255, 255]
|
||||
|
||||
self._stick.set_color(red=self._rgb_color[0],
|
||||
green=self._rgb_color[1],
|
||||
blue=self._rgb_color[2])
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off """
|
||||
self._stick.turn_off()
|
112
homeassistant/components/light/rfxtrx.py
Normal file
112
homeassistant/components/light/rfxtrx.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
"""
|
||||
homeassistant.components.light.rfxtrx
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for RFXtrx lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.rfxtrx.html
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.rfxtrx as rfxtrx
|
||||
import RFXtrx as rfxtrxmod
|
||||
|
||||
from homeassistant.components.light import Light
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DEPENDENCIES = ['rfxtrx']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Setup the RFXtrx platform. """
|
||||
lights = []
|
||||
devices = config.get('devices', None)
|
||||
if devices:
|
||||
for entity_id, entity_info in devices.items():
|
||||
if entity_id not in rfxtrx.RFX_DEVICES:
|
||||
_LOGGER.info("Add %s rfxtrx.light", entity_info['name'])
|
||||
rfxobject = rfxtrx.get_rfx_object(entity_info['packetid'])
|
||||
new_light = RfxtrxLight(entity_info['name'], rfxobject, False)
|
||||
rfxtrx.RFX_DEVICES[entity_id] = new_light
|
||||
lights.append(new_light)
|
||||
|
||||
add_devices_callback(lights)
|
||||
|
||||
def light_update(event):
|
||||
""" Callback for light updates from the RFXtrx gateway. """
|
||||
if not isinstance(event.device, rfxtrxmod.LightingDevice):
|
||||
return
|
||||
|
||||
# Add entity if not exist and the automatic_add is True
|
||||
entity_id = slugify(event.device.id_string.lower())
|
||||
if entity_id not in rfxtrx.RFX_DEVICES:
|
||||
automatic_add = config.get('automatic_add', False)
|
||||
if not automatic_add:
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
"Automatic add %s rfxtrx.light (Class: %s Sub: %s)",
|
||||
entity_id,
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype
|
||||
)
|
||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
entity_name = "%s : %s" % (entity_id, pkt_id)
|
||||
new_light = RfxtrxLight(entity_name, event, False)
|
||||
rfxtrx.RFX_DEVICES[entity_id] = new_light
|
||||
add_devices_callback([new_light])
|
||||
|
||||
# Check if entity exists or previously added automatically
|
||||
if entity_id in rfxtrx.RFX_DEVICES:
|
||||
if event.values['Command'] == 'On'\
|
||||
or event.values['Command'] == 'Off':
|
||||
if event.values['Command'] == 'On':
|
||||
rfxtrx.RFX_DEVICES[entity_id].turn_on()
|
||||
else:
|
||||
rfxtrx.RFX_DEVICES[entity_id].turn_off()
|
||||
|
||||
# Subscribe to main rfxtrx events
|
||||
if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
||||
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(light_update)
|
||||
|
||||
|
||||
class RfxtrxLight(Light):
|
||||
""" Provides a RFXtrx light. """
|
||||
def __init__(self, name, event, state):
|
||||
self._name = name
|
||||
self._event = event
|
||||
self._state = state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a light. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the light if any. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if light is on. """
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the light on. """
|
||||
|
||||
if hasattr(self, '_event') and self._event:
|
||||
self._event.device.send_on(rfxtrx.RFXOBJECT.transport)
|
||||
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the light off. """
|
||||
|
||||
if hasattr(self, '_event') and self._event:
|
||||
self._event.device.send_off(rfxtrx.RFXOBJECT.transport)
|
||||
|
||||
self._state = False
|
||||
self.update_ha_state()
|
|
@ -31,6 +31,7 @@ DISCOVERY_PLATFORMS = {
|
|||
}
|
||||
|
||||
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
|
||||
SERVICE_PLAY_MEDIA = 'play_media'
|
||||
|
||||
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
|
||||
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
|
||||
|
@ -46,6 +47,8 @@ ATTR_MEDIA_TRACK = 'media_track'
|
|||
ATTR_MEDIA_SERIES_TITLE = 'media_series_title'
|
||||
ATTR_MEDIA_SEASON = 'media_season'
|
||||
ATTR_MEDIA_EPISODE = 'media_episode'
|
||||
ATTR_MEDIA_CHANNEL = 'media_channel'
|
||||
ATTR_MEDIA_PLAYLIST = 'media_playlist'
|
||||
ATTR_APP_ID = 'app_id'
|
||||
ATTR_APP_NAME = 'app_name'
|
||||
ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
|
||||
|
@ -53,6 +56,9 @@ ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
|
|||
MEDIA_TYPE_MUSIC = 'music'
|
||||
MEDIA_TYPE_TVSHOW = 'tvshow'
|
||||
MEDIA_TYPE_VIDEO = 'movie'
|
||||
MEDIA_TYPE_EPISODE = 'episode'
|
||||
MEDIA_TYPE_CHANNEL = 'channel'
|
||||
MEDIA_TYPE_PLAYLIST = 'playlist'
|
||||
|
||||
SUPPORT_PAUSE = 1
|
||||
SUPPORT_SEEK = 2
|
||||
|
@ -63,6 +69,7 @@ SUPPORT_NEXT_TRACK = 32
|
|||
SUPPORT_YOUTUBE = 64
|
||||
SUPPORT_TURN_ON = 128
|
||||
SUPPORT_TURN_OFF = 256
|
||||
SUPPORT_PLAY_MEDIA = 512
|
||||
|
||||
YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg'
|
||||
|
||||
|
@ -76,6 +83,7 @@ SERVICE_TO_METHOD = {
|
|||
SERVICE_MEDIA_PAUSE: 'media_pause',
|
||||
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
|
||||
SERVICE_PLAY_MEDIA: 'play_media',
|
||||
}
|
||||
|
||||
ATTR_TO_PROPERTY = [
|
||||
|
@ -92,6 +100,8 @@ ATTR_TO_PROPERTY = [
|
|||
ATTR_MEDIA_SERIES_TITLE,
|
||||
ATTR_MEDIA_SEASON,
|
||||
ATTR_MEDIA_EPISODE,
|
||||
ATTR_MEDIA_CHANNEL,
|
||||
ATTR_MEDIA_PLAYLIST,
|
||||
ATTR_APP_ID,
|
||||
ATTR_APP_NAME,
|
||||
ATTR_SUPPORTED_MEDIA_COMMANDS,
|
||||
|
@ -180,6 +190,16 @@ def media_previous_track(hass, entity_id=None):
|
|||
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data)
|
||||
|
||||
|
||||
def play_media(hass, media_type, media_id, entity_id=None):
|
||||
""" Send the media player the command for playing media. """
|
||||
data = {"media_type": media_type, "media_id": media_id}
|
||||
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Track states and offer events for media_players. """
|
||||
component = EntityComponent(
|
||||
|
@ -275,6 +295,23 @@ def setup(hass, config):
|
|||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
def play_media_service(service):
|
||||
""" Plays specified media_id on the media player. """
|
||||
media_type = service.data.get('media_type')
|
||||
media_id = service.data.get('media_id')
|
||||
|
||||
if media_type is None:
|
||||
return
|
||||
|
||||
if media_id is None:
|
||||
return
|
||||
|
||||
for player in component.extract_from_service(service):
|
||||
player.play_media(media_type, media_id)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, "start_fireplace",
|
||||
lambda service: play_youtube_video_service(service, "eyU3bRy2x44"),
|
||||
|
@ -289,6 +326,10 @@ def setup(hass, config):
|
|||
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service,
|
||||
descriptions.get(SERVICE_YOUTUBE_VIDEO))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_PLAY_MEDIA, play_media_service,
|
||||
descriptions.get(SERVICE_PLAY_MEDIA))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -373,6 +414,16 @@ class MediaPlayerDevice(Entity):
|
|||
""" Episode of current playing media. (TV Show only) """
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_channel(self):
|
||||
""" Channel currently playing. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_playlist(self):
|
||||
""" Title of Playlist currently playing. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
""" ID of the current running app. """
|
||||
|
@ -433,6 +484,10 @@ class MediaPlayerDevice(Entity):
|
|||
""" Plays a YouTube media. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
""" Plays a piece of media. """
|
||||
raise NotImplementedError()
|
||||
|
||||
# No need to overwrite these.
|
||||
@property
|
||||
def support_pause(self):
|
||||
|
@ -469,6 +524,11 @@ class MediaPlayerDevice(Entity):
|
|||
""" Boolean if YouTube is supported. """
|
||||
return bool(self.supported_media_commands & SUPPORT_YOUTUBE)
|
||||
|
||||
@property
|
||||
def support_play_media(self):
|
||||
""" Boolean if play media command supported. """
|
||||
return bool(self.supported_media_commands & SUPPORT_PLAY_MEDIA)
|
||||
|
||||
def volume_up(self):
|
||||
""" volume_up media player. """
|
||||
if self.volume_level < 1:
|
||||
|
|
|
@ -90,6 +90,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class CastDevice(MediaPlayerDevice):
|
||||
""" Represents a Cast device on the network. """
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
def __init__(self, host):
|
||||
|
|
|
@ -35,9 +35,10 @@ URL of your running version of iTunes-API. Example: http://192.168.1.50:8181
|
|||
import logging
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, SUPPORT_SEEK,
|
||||
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
|
||||
MediaPlayerDevice, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_PAUSE,
|
||||
SUPPORT_SEEK, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON,
|
||||
SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA,
|
||||
ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_MEDIA_COMMANDS)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_ON)
|
||||
|
@ -47,7 +48,8 @@ import requests
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
|
||||
SUPPORT_PLAY_MEDIA
|
||||
|
||||
SUPPORT_AIRPLAY = SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||
|
||||
|
@ -118,6 +120,20 @@ class Itunes(object):
|
|||
""" Skips back and returns the current state. """
|
||||
return self._command('previous')
|
||||
|
||||
def play_playlist(self, playlist_id_or_name):
|
||||
""" Sets a playlist to be current and returns the current state. """
|
||||
response = self._request('GET', '/playlists')
|
||||
playlists = response.get('playlists', [])
|
||||
|
||||
found_playlists = \
|
||||
[playlist for playlist in playlists if
|
||||
(playlist_id_or_name in [playlist["name"], playlist["id"]])]
|
||||
|
||||
if len(found_playlists) > 0:
|
||||
playlist = found_playlists[0]
|
||||
path = '/playlists/' + playlist['id'] + '/play'
|
||||
return self._request('PUT', path)
|
||||
|
||||
def artwork_url(self):
|
||||
""" Returns a URL of the current track's album art. """
|
||||
return self._base_url + '/artwork'
|
||||
|
@ -294,6 +310,11 @@ class ItunesDevice(MediaPlayerDevice):
|
|||
""" Album of current playing media. (Music track only) """
|
||||
return self.current_album
|
||||
|
||||
@property
|
||||
def media_playlist(self):
|
||||
""" Title of the currently playing playlist. """
|
||||
return self.current_playlist
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
|
@ -329,6 +350,12 @@ class ItunesDevice(MediaPlayerDevice):
|
|||
response = self.client.previous()
|
||||
self.update_state(response)
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
""" play_media media player. """
|
||||
if media_type == MEDIA_TYPE_PLAYLIST:
|
||||
response = self.client.play_playlist(media_id)
|
||||
self.update_state(response)
|
||||
|
||||
|
||||
class AirPlayDevice(MediaPlayerDevice):
|
||||
""" Represents an AirPlay device via an iTunes-API instance. """
|
||||
|
|
|
@ -167,7 +167,7 @@ class KodiDevice(MediaPlayerDevice):
|
|||
def media_content_id(self):
|
||||
""" Content ID of current playing media. """
|
||||
if self._item is not None:
|
||||
return self._item['uniqueid']
|
||||
return self._item.get('uniqueid', None)
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
|
|
|
@ -1,34 +1,11 @@
|
|||
"""
|
||||
homeassistant.components.media_player.plex
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides an interface to the Plex API.
|
||||
|
||||
Provides an interface to the Plex API
|
||||
|
||||
Configuration:
|
||||
|
||||
To use Plex add something like this to your configuration:
|
||||
|
||||
media_player:
|
||||
platform: plex
|
||||
name: plex_server
|
||||
user: plex
|
||||
password: my_secure_password
|
||||
|
||||
Variables:
|
||||
|
||||
name
|
||||
*Required
|
||||
The name of the backend device (Under Plex Media Server > settings > server).
|
||||
|
||||
user
|
||||
*Required
|
||||
The Plex username
|
||||
|
||||
password
|
||||
*Required
|
||||
The Plex password
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.plex.html
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
|
@ -49,10 +26,8 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
|
||||
# pylint: disable=abstract-method, unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the plex platform. """
|
||||
from plexapi.myplex import MyPlexUser
|
||||
|
@ -68,7 +43,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update_devices():
|
||||
""" Updates the devices objects """
|
||||
""" Updates the devices objects. """
|
||||
try:
|
||||
devices = plexuser.devices()
|
||||
except BadRequest:
|
||||
|
@ -94,7 +69,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update_sessions():
|
||||
""" Updates the sessions objects """
|
||||
""" Updates the sessions objects. """
|
||||
try:
|
||||
sessions = plexserver.sessions()
|
||||
except BadRequest:
|
||||
|
@ -113,7 +88,6 @@ class PlexClient(MediaPlayerDevice):
|
|||
""" Represents a Plex device. """
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
def __init__(self, device, plex_sessions, update_devices, update_sessions):
|
||||
self.plex_sessions = plex_sessions
|
||||
self.update_devices = update_devices
|
||||
|
@ -121,12 +95,12 @@ class PlexClient(MediaPlayerDevice):
|
|||
self.set_device(device)
|
||||
|
||||
def set_device(self, device):
|
||||
""" Sets the device property """
|
||||
""" Sets the device property. """
|
||||
self.device = device
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
""" Returns the session, if any """
|
||||
""" Returns the session, if any. """
|
||||
if self.device.clientIdentifier not in self.plex_sessions:
|
||||
return None
|
||||
|
||||
|
@ -196,21 +170,21 @@ class PlexClient(MediaPlayerDevice):
|
|||
|
||||
@property
|
||||
def media_season(self):
|
||||
""" Season of curent playing media. (TV Show only) """
|
||||
""" Season of curent playing media (TV Show only). """
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.seasons()[0].index
|
||||
|
||||
@property
|
||||
def media_series_title(self):
|
||||
""" Series title of current playing media. (TV Show only)"""
|
||||
""" Series title of current playing media (TV Show only). """
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.grandparentTitle
|
||||
|
||||
@property
|
||||
def media_episode(self):
|
||||
""" Episode of current playing media. (TV Show only) """
|
||||
""" Episode of current playing media (TV Show only). """
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.index
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
"""
|
||||
homeassistant.components.notify.mail
|
||||
homeassistant.components.notify.smtp
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Mail (SMTP) notification service.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Mail notifier you will need to add something like the following
|
||||
To use the smtp notifier you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
notify:
|
||||
platform: mail
|
||||
platform: smtp
|
||||
server: MAIL_SERVER
|
||||
port: YOUR_SMTP_PORT
|
||||
sender: SENDER_EMAIL_ADDRESS
|
||||
|
|
66
homeassistant/components/notify/telegram.py
Normal file
66
homeassistant/components/notify/telegram.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
"""
|
||||
homeassistant.components.notify.telegram
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Telegram platform for notify component.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.telegram.html
|
||||
"""
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.notify import (
|
||||
DOMAIN, ATTR_TITLE, BaseNotificationService)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['python-telegram-bot==2.8.7']
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
""" Get the Telegram notification service. """
|
||||
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_API_KEY, 'chat_id']},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
try:
|
||||
import telegram
|
||||
except ImportError:
|
||||
_LOGGER.exception(
|
||||
"Unable to import python-telegram-bot. "
|
||||
"Did you maybe not install the 'python-telegram-bot' package?")
|
||||
return None
|
||||
|
||||
try:
|
||||
bot = telegram.Bot(token=config[DOMAIN][CONF_API_KEY])
|
||||
username = bot.getMe()['username']
|
||||
_LOGGER.info("Telegram bot is' %s'", username)
|
||||
except urllib.error.HTTPError:
|
||||
_LOGGER.error("Please check your access token.")
|
||||
return None
|
||||
|
||||
return TelegramNotificationService(
|
||||
config[DOMAIN][CONF_API_KEY],
|
||||
config[DOMAIN]['chat_id'])
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class TelegramNotificationService(BaseNotificationService):
|
||||
""" Implements notification service for Telegram. """
|
||||
|
||||
def __init__(self, api_key, chat_id):
|
||||
import telegram
|
||||
self._api_key = api_key
|
||||
self._chat_id = chat_id
|
||||
self.bot = telegram.Bot(token=self._api_key)
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
""" Send a message to a user. """
|
||||
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
|
||||
self.bot.sendMessage(chat_id=self._chat_id,
|
||||
text=title + " " + message)
|
89
homeassistant/components/rfxtrx.py
Normal file
89
homeassistant/components/rfxtrx.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
"""
|
||||
homeassistant.components.rfxtrx
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides support for RFXtrx components.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/rfxtrx.html
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/0.2.zip' +
|
||||
'#RFXtrx==0.2']
|
||||
|
||||
DOMAIN = "rfxtrx"
|
||||
CONF_DEVICE = 'device'
|
||||
CONF_DEBUG = 'debug'
|
||||
RECEIVED_EVT_SUBSCRIBERS = []
|
||||
RFX_DEVICES = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
RFXOBJECT = None
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup the RFXtrx component. """
|
||||
|
||||
# Declare the Handle event
|
||||
def handle_receive(event):
|
||||
""" Callback all subscribers for RFXtrx gateway. """
|
||||
|
||||
# Log RFXCOM event
|
||||
entity_id = slugify(event.device.id_string.lower())
|
||||
packet_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
entity_name = "%s : %s" % (entity_id, packet_id)
|
||||
_LOGGER.info("Receive RFXCOM event from %s => %s",
|
||||
event.device, entity_name)
|
||||
|
||||
# Callback to HA registered components
|
||||
for subscriber in RECEIVED_EVT_SUBSCRIBERS:
|
||||
subscriber(event)
|
||||
|
||||
# Try to load the RFXtrx module
|
||||
try:
|
||||
import RFXtrx as rfxtrxmod
|
||||
except ImportError:
|
||||
_LOGGER.exception("Failed to import rfxtrx")
|
||||
return False
|
||||
|
||||
# Init the rfxtrx module
|
||||
global RFXOBJECT
|
||||
|
||||
if CONF_DEVICE not in config[DOMAIN]:
|
||||
_LOGGER.exception(
|
||||
"can found device parameter in %s YAML configuration section",
|
||||
DOMAIN
|
||||
)
|
||||
return False
|
||||
|
||||
device = config[DOMAIN][CONF_DEVICE]
|
||||
debug = config[DOMAIN].get(CONF_DEBUG, False)
|
||||
|
||||
RFXOBJECT = rfxtrxmod.Core(device, handle_receive, debug=debug)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_rfx_object(packetid):
|
||||
""" Return the RFXObject with the packetid. """
|
||||
try:
|
||||
import RFXtrx as rfxtrxmod
|
||||
except ImportError:
|
||||
_LOGGER.exception("Failed to import rfxtrx")
|
||||
return False
|
||||
|
||||
binarypacket = bytearray.fromhex(packetid)
|
||||
|
||||
pkt = rfxtrxmod.lowlevel.parse(binarypacket)
|
||||
if pkt is not None:
|
||||
if isinstance(pkt, rfxtrxmod.lowlevel.SensorPacket):
|
||||
obj = rfxtrxmod.SensorEvent(pkt)
|
||||
elif isinstance(pkt, rfxtrxmod.lowlevel.Status):
|
||||
obj = rfxtrxmod.StatusEvent(pkt)
|
||||
else:
|
||||
obj = rfxtrxmod.ControlEvent(pkt)
|
||||
|
||||
return obj
|
||||
|
||||
return None
|
|
@ -3,51 +3,11 @@ homeassistant.components.sensor.arest
|
|||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The arest sensor will consume an exposed aREST API of a device.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the arest sensor you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
sensor:
|
||||
platform: arest
|
||||
resource: http://IP_ADDRESS
|
||||
monitored_variables:
|
||||
- name: temperature
|
||||
unit: '°C'
|
||||
- name: humidity
|
||||
unit: '%'
|
||||
|
||||
Variables:
|
||||
|
||||
resource:
|
||||
*Required
|
||||
IP address of the device that is exposing an aREST API.
|
||||
|
||||
These are the variables for the monitored_variables array:
|
||||
|
||||
name
|
||||
*Required
|
||||
The name of the variable you wish to monitor.
|
||||
|
||||
unit
|
||||
*Optional
|
||||
Defines the units of measurement of the sensor, if any.
|
||||
|
||||
Details for the API: http://arest.io
|
||||
|
||||
Format of a default JSON response by aREST:
|
||||
{
|
||||
"variables":{
|
||||
"temperature":21,
|
||||
"humidity":89
|
||||
},
|
||||
"id":"device008",
|
||||
"name":"Bedroom",
|
||||
"connected":true
|
||||
}
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.arest.html
|
||||
"""
|
||||
import logging
|
||||
from requests import get, exceptions
|
||||
import requests
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
|
@ -58,36 +18,42 @@ _LOGGER = logging.getLogger(__name__)
|
|||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
CONF_RESOURCE = 'resource'
|
||||
CONF_MONITORED_VARIABLES = 'monitored_variables'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the aREST sensor. """
|
||||
|
||||
resource = config.get('resource', None)
|
||||
resource = config.get(CONF_RESOURCE)
|
||||
var_conf = config.get(CONF_MONITORED_VARIABLES)
|
||||
|
||||
if None in (resource, var_conf):
|
||||
_LOGGER.error('Not all required config keys present: %s',
|
||||
', '.join((CONF_RESOURCE, CONF_MONITORED_VARIABLES)))
|
||||
return False
|
||||
|
||||
try:
|
||||
response = get(resource, timeout=10)
|
||||
except exceptions.MissingSchema:
|
||||
response = requests.get(resource, timeout=10).json()
|
||||
except requests.exceptions.MissingSchema:
|
||||
_LOGGER.error("Missing resource or schema in configuration. "
|
||||
"Add http:// to your URL.")
|
||||
return False
|
||||
except exceptions.ConnectionError:
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to device. "
|
||||
"Please check the IP address in the configuration file.")
|
||||
return False
|
||||
|
||||
rest = ArestData(resource)
|
||||
arest = ArestData(resource)
|
||||
|
||||
dev = []
|
||||
for variable in config['monitored_variables']:
|
||||
if 'unit' not in variable:
|
||||
variable['unit'] = ' '
|
||||
if variable['name'] not in response.json()['variables']:
|
||||
if variable['name'] not in response['variables']:
|
||||
_LOGGER.error('Variable: "%s" does not exist', variable['name'])
|
||||
else:
|
||||
dev.append(ArestSensor(rest,
|
||||
response.json()['name'],
|
||||
variable['name'],
|
||||
variable['unit']))
|
||||
continue
|
||||
|
||||
dev.append(ArestSensor(arest, response['name'], variable['name'],
|
||||
variable.get('unit')))
|
||||
|
||||
add_devices(dev)
|
||||
|
||||
|
@ -95,8 +61,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class ArestSensor(Entity):
|
||||
""" Implements an aREST sensor. """
|
||||
|
||||
def __init__(self, rest, location, variable, unit_of_measurement):
|
||||
self.rest = rest
|
||||
def __init__(self, arest, location, variable, unit_of_measurement):
|
||||
self.arest = arest
|
||||
self._name = '{} {}'.format(location.title(), variable.title())
|
||||
self._variable = variable
|
||||
self._state = 'n/a'
|
||||
|
@ -116,17 +82,16 @@ class ArestSensor(Entity):
|
|||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data from aREST API and updates the state. """
|
||||
self.rest.update()
|
||||
values = self.rest.data
|
||||
values = self.arest.data
|
||||
|
||||
if 'error' in values:
|
||||
self._state = values['error']
|
||||
return values['error']
|
||||
else:
|
||||
self._state = values[self._variable]
|
||||
return values.get(self._variable, 'n/a')
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data from aREST API. """
|
||||
self.arest.update()
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
@ -135,16 +100,14 @@ class ArestData(object):
|
|||
|
||||
def __init__(self, resource):
|
||||
self.resource = resource
|
||||
self.data = dict()
|
||||
self.data = {}
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from aREST device. """
|
||||
try:
|
||||
response = get(self.resource, timeout=10)
|
||||
if 'error' in self.data:
|
||||
del self.data['error']
|
||||
response = requests.get(self.resource, timeout=10)
|
||||
self.data = response.json()['variables']
|
||||
except exceptions.ConnectionError:
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to device. Is device offline?")
|
||||
self.data['error'] = 'n/a'
|
||||
self.data = {'error': 'error fetching'}
|
||||
|
|
|
@ -3,30 +3,19 @@ homeassistant.components.sensor.rfxtrx
|
|||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Shows sensor values from RFXtrx sensors.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the rfxtrx sensors you will need to add something like the following to
|
||||
your configuration.yaml file.
|
||||
|
||||
sensor:
|
||||
platform: rfxtrx
|
||||
device: PATH_TO_DEVICE
|
||||
|
||||
Variables:
|
||||
|
||||
device
|
||||
*Required
|
||||
Path to your RFXtrx device.
|
||||
E.g. /dev/serial/by-id/usb-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.rfxtrx.html
|
||||
"""
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from homeassistant.const import (TEMP_CELCIUS)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.components.rfxtrx as rfxtrx
|
||||
from RFXtrx import SensorEvent
|
||||
from homeassistant.util import slugify
|
||||
|
||||
REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/' +
|
||||
'ec7a1aaddf8270db6e5da1c13d58c1547effd7cf.zip#RFXtrx==0.15']
|
||||
DEPENDENCIES = ['rfxtrx']
|
||||
|
||||
DATA_TYPES = OrderedDict([
|
||||
('Temperature', TEMP_CELCIUS),
|
||||
|
@ -34,32 +23,30 @@ DATA_TYPES = OrderedDict([
|
|||
('Barometer', ''),
|
||||
('Wind direction', ''),
|
||||
('Rain rate', '')])
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Setup the RFXtrx platform. """
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
sensors = {} # keep track of sensors added to HA
|
||||
|
||||
def sensor_update(event):
|
||||
""" Callback for sensor updates from the RFXtrx gateway. """
|
||||
if event.device.id_string in sensors:
|
||||
sensors[event.device.id_string].event = event
|
||||
else:
|
||||
logger.info("adding new sensor: %s", event.device.type_string)
|
||||
new_sensor = RfxtrxSensor(event)
|
||||
sensors[event.device.id_string] = new_sensor
|
||||
add_devices([new_sensor])
|
||||
try:
|
||||
import RFXtrx as rfxtrx
|
||||
except ImportError:
|
||||
logger.exception(
|
||||
"Failed to import rfxtrx")
|
||||
return False
|
||||
if isinstance(event.device, SensorEvent):
|
||||
entity_id = slugify(event.device.id_string.lower())
|
||||
|
||||
device = config.get("device", "")
|
||||
rfxtrx.Core(device, sensor_update)
|
||||
# Add entity if not exist and the automatic_add is True
|
||||
if entity_id not in rfxtrx.RFX_DEVICES:
|
||||
automatic_add = config.get('automatic_add', True)
|
||||
if automatic_add:
|
||||
_LOGGER.info("Automatic add %s rfxtrx.sensor", entity_id)
|
||||
new_sensor = RfxtrxSensor(event)
|
||||
rfxtrx.RFX_DEVICES[entity_id] = new_sensor
|
||||
add_devices_callback([new_sensor])
|
||||
else:
|
||||
rfxtrx.RFX_DEVICES[entity_id].event = event
|
||||
|
||||
if sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
||||
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(sensor_update)
|
||||
|
||||
|
||||
class RfxtrxSensor(Entity):
|
||||
|
@ -67,7 +54,6 @@ class RfxtrxSensor(Entity):
|
|||
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
|
||||
self._unit_of_measurement = None
|
||||
self._data_type = None
|
||||
for data_type in DATA_TYPES:
|
||||
|
@ -86,13 +72,14 @@ class RfxtrxSensor(Entity):
|
|||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
if self._data_type:
|
||||
return self.event.values[self._data_type]
|
||||
return None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Get the mame of the sensor. """
|
||||
""" Get the name of the sensor. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
|
|
|
@ -3,38 +3,9 @@ homeassistant.components.switch.arest
|
|||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The arest switch can control the digital pins of a device running with the
|
||||
aREST RESTful framework for Arduino, the ESP8266, and the Raspberry Pi.
|
||||
Only tested with Arduino boards so far.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the arest switch you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
sensor:
|
||||
platform: arest
|
||||
resource: http://IP_ADDRESS
|
||||
pins:
|
||||
11:
|
||||
name: Fan Office
|
||||
12:
|
||||
name: Light Desk
|
||||
|
||||
Variables:
|
||||
|
||||
resource:
|
||||
*Required
|
||||
IP address of the device that is exposing an aREST API.
|
||||
|
||||
pins:
|
||||
The number of the digital pin to switch.
|
||||
|
||||
These are the variables for the pins array:
|
||||
|
||||
name
|
||||
*Required
|
||||
The name for the pin that will be used in the frontend.
|
||||
|
||||
Details for the API: http://arest.io
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.arest.html
|
||||
"""
|
||||
import logging
|
||||
from requests import get, exceptions
|
||||
|
|
112
homeassistant/components/switch/rfxtrx.py
Normal file
112
homeassistant/components/switch/rfxtrx.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
"""
|
||||
homeassistant.components.switch.rfxtrx
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for RFXtrx switches.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.rfxtrx.html
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.rfxtrx as rfxtrx
|
||||
from RFXtrx import LightingDevice
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DEPENDENCIES = ['rfxtrx']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Setup the RFXtrx platform. """
|
||||
|
||||
# Add switch from config file
|
||||
switchs = []
|
||||
devices = config.get('devices')
|
||||
if devices:
|
||||
for entity_id, entity_info in devices.items():
|
||||
if entity_id not in rfxtrx.RFX_DEVICES:
|
||||
_LOGGER.info("Add %s rfxtrx.switch", entity_info['name'])
|
||||
rfxobject = rfxtrx.get_rfx_object(entity_info['packetid'])
|
||||
newswitch = RfxtrxSwitch(entity_info['name'], rfxobject, False)
|
||||
rfxtrx.RFX_DEVICES[entity_id] = newswitch
|
||||
switchs.append(newswitch)
|
||||
|
||||
add_devices_callback(switchs)
|
||||
|
||||
def switch_update(event):
|
||||
""" Callback for sensor updates from the RFXtrx gateway. """
|
||||
if isinstance(event.device, LightingDevice):
|
||||
return
|
||||
|
||||
# Add entity if not exist and the automatic_add is True
|
||||
entity_id = slugify(event.device.id_string.lower())
|
||||
if entity_id not in rfxtrx.RFX_DEVICES:
|
||||
automatic_add = config.get('automatic_add', False)
|
||||
if not automatic_add:
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
"Automatic add %s rfxtrx.switch (Class: %s Sub: %s)",
|
||||
entity_id,
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype
|
||||
)
|
||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
entity_name = "%s : %s" % (entity_id, pkt_id)
|
||||
new_switch = RfxtrxSwitch(entity_name, event, False)
|
||||
rfxtrx.RFX_DEVICES[entity_id] = new_switch
|
||||
add_devices_callback([new_switch])
|
||||
|
||||
# Check if entity exists or previously added automatically
|
||||
if entity_id in rfxtrx.RFX_DEVICES:
|
||||
if event.values['Command'] == 'On'\
|
||||
or event.values['Command'] == 'Off':
|
||||
if event.values['Command'] == 'On':
|
||||
rfxtrx.RFX_DEVICES[entity_id].turn_on()
|
||||
else:
|
||||
rfxtrx.RFX_DEVICES[entity_id].turn_off()
|
||||
|
||||
# Subscribe to main rfxtrx events
|
||||
if switch_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
||||
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(switch_update)
|
||||
|
||||
|
||||
class RfxtrxSwitch(SwitchDevice):
|
||||
""" Provides a RFXtrx switch. """
|
||||
def __init__(self, name, event, state):
|
||||
self._name = name
|
||||
self._event = event
|
||||
self._state = state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a RFXtrx switch. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
if self._event:
|
||||
self._event.device.send_on(rfxtrx.RFXOBJECT.transport)
|
||||
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
if self._event:
|
||||
self._event.device.send_off(rfxtrx.RFXOBJECT.transport)
|
||||
|
||||
self._state = False
|
||||
self.update_ha_state()
|
|
@ -1,7 +1,7 @@
|
|||
# coding: utf-8
|
||||
""" Constants used by Home Assistant components. """
|
||||
|
||||
__version__ = "0.7.4"
|
||||
__version__ = "0.7.5"
|
||||
|
||||
# Can be used to specify a catch all when registering state or event listeners.
|
||||
MATCH_ALL = '*'
|
||||
|
|
|
@ -13,6 +13,8 @@ from homeassistant.const import (
|
|||
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
|
||||
STATE_PLAYING, STATE_PAUSED, ATTR_ENTITY_ID)
|
||||
|
||||
from homeassistant.components.media_player import (SERVICE_PLAY_MEDIA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -57,7 +59,11 @@ def reproduce_state(hass, states, blocking=False):
|
|||
state.entity_id)
|
||||
continue
|
||||
|
||||
if state.domain == 'media_player' and state.state == STATE_PAUSED:
|
||||
if state.domain == 'media_player' and state.attributes and \
|
||||
'media_type' in state.attributes and \
|
||||
'media_id' in state.attributes:
|
||||
service = SERVICE_PLAY_MEDIA
|
||||
elif state.domain == 'media_player' and state.state == STATE_PAUSED:
|
||||
service = SERVICE_MEDIA_PAUSE
|
||||
elif state.domain == 'media_player' and state.state == STATE_PLAYING:
|
||||
service = SERVICE_MEDIA_PLAY
|
||||
|
|
|
@ -233,35 +233,42 @@ class Throttle(object):
|
|||
self.limit_no_throttle = limit_no_throttle
|
||||
|
||||
def __call__(self, method):
|
||||
lock = threading.Lock()
|
||||
|
||||
if self.limit_no_throttle is not None:
|
||||
method = Throttle(self.limit_no_throttle)(method)
|
||||
|
||||
# We want to be able to differentiate between function and method calls
|
||||
# All methods have the classname in their qualname seperated by a '.'
|
||||
# Functions have a '.' in their qualname if defined inline, but will
|
||||
# be prefixed by '.<locals>.' so we strip that out.
|
||||
is_func = '.' not in method.__qualname__.split('.<locals>.')[-1]
|
||||
|
||||
@wraps(method)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""
|
||||
Wrapper that allows wrapped to be called only once per min_time.
|
||||
If we cannot acquire the lock, it is running so return None.
|
||||
"""
|
||||
if not lock.acquire(False):
|
||||
# pylint: disable=protected-access
|
||||
host = wrapper if is_func else args[0]
|
||||
if not hasattr(host, '_throttle_lock'):
|
||||
host._throttle_lock = threading.Lock()
|
||||
|
||||
if not host._throttle_lock.acquire(False):
|
||||
return None
|
||||
|
||||
last_call = getattr(host, '_throttle_last_call', None)
|
||||
# Check if method is never called or no_throttle is given
|
||||
force = not last_call or kwargs.pop('no_throttle', False)
|
||||
|
||||
try:
|
||||
last_call = wrapper.last_call
|
||||
|
||||
# Check if method is never called or no_throttle is given
|
||||
force = not last_call or kwargs.pop('no_throttle', False)
|
||||
|
||||
if force or utcnow() - last_call > self.min_time:
|
||||
result = method(*args, **kwargs)
|
||||
wrapper.last_call = utcnow()
|
||||
host._throttle_last_call = utcnow()
|
||||
return result
|
||||
else:
|
||||
return None
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
wrapper.last_call = None
|
||||
host._throttle_lock.release()
|
||||
|
||||
return wrapper
|
||||
|
||||
|
|
|
@ -10,77 +10,77 @@ vincenty==0.1.2
|
|||
# Sun (sun)
|
||||
astral==0.8.1
|
||||
|
||||
# Philips Hue library (lights.hue)
|
||||
# Philips Hue (lights.hue)
|
||||
phue==0.8
|
||||
|
||||
# Limitlessled/Easybulb/Milight library (lights.limitlessled)
|
||||
# Limitlessled/Easybulb/Milight (lights.limitlessled)
|
||||
ledcontroller==1.1.0
|
||||
|
||||
# Chromecast bindings (media_player.cast)
|
||||
# Chromecast (media_player.cast)
|
||||
pychromecast==0.6.12
|
||||
|
||||
# Keyboard (keyboard)
|
||||
pyuserinput==0.1.9
|
||||
|
||||
# Tellstick bindings (*.tellstick)
|
||||
# Tellstick (*.tellstick)
|
||||
tellcore-py==1.1.2
|
||||
|
||||
# Nmap bindings (device_tracker.nmap)
|
||||
# Nmap (device_tracker.nmap)
|
||||
python-nmap==0.4.3
|
||||
|
||||
# PushBullet bindings (notify.pushbullet)
|
||||
# PushBullet (notify.pushbullet)
|
||||
pushbullet.py==0.7.1
|
||||
|
||||
# Nest Thermostat bindings (thermostat.nest)
|
||||
# Nest Thermostat (thermostat.nest)
|
||||
python-nest==2.6.0
|
||||
|
||||
# Z-Wave (*.zwave)
|
||||
pydispatcher==2.0.5
|
||||
|
||||
# ISY994 bindings (*.isy994)
|
||||
# ISY994 (isy994)
|
||||
PyISY==1.0.5
|
||||
|
||||
# PSutil (sensor.systemmonitor)
|
||||
psutil==3.0.0
|
||||
|
||||
# Pushover bindings (notify.pushover)
|
||||
# Pushover (notify.pushover)
|
||||
python-pushover==0.2
|
||||
|
||||
# Transmission Torrent Client (*.transmission)
|
||||
transmissionrpc==0.11
|
||||
|
||||
# OpenWeatherMap Web API (sensor.openweathermap)
|
||||
# OpenWeatherMap (sensor.openweathermap)
|
||||
pyowm==2.2.1
|
||||
|
||||
# XMPP Bindings (notify.xmpp)
|
||||
# XMPP (notify.xmpp)
|
||||
sleekxmpp==1.3.1
|
||||
dnspython3==1.12.0
|
||||
|
||||
# Blockchain (sensor.bitcoin)
|
||||
blockchain==1.1.2
|
||||
|
||||
# MPD Bindings (media_player.mpd)
|
||||
# Music Player Daemon (media_player.mpd)
|
||||
python-mpd2==0.5.4
|
||||
|
||||
# Hikvision (switch.hikvisioncam)
|
||||
hikvision==0.4
|
||||
|
||||
# console log coloring
|
||||
# Console log coloring
|
||||
colorlog==2.6.0
|
||||
|
||||
# JSON-RPC interface (media_player.kodi)
|
||||
jsonrpc-requests==0.1
|
||||
|
||||
# Forecast.io Bindings (sensor.forecast)
|
||||
# Forecast.io (sensor.forecast)
|
||||
python-forecastio==1.3.3
|
||||
|
||||
# Firmata Bindings (*.arduino)
|
||||
# Firmata (*.arduino)
|
||||
PyMata==2.07a
|
||||
|
||||
# Rfxtrx sensor (sensor.rfxtrx)
|
||||
# Rfxtrx (rfxtrx)
|
||||
https://github.com/Danielhiversen/pyRFXtrx/archive/ec7a1aaddf8270db6e5da1c13d58c1547effd7cf.zip#RFXtrx==0.15
|
||||
|
||||
# Mysensors
|
||||
# Mysensors (sensor.mysensors)
|
||||
https://github.com/theolind/pymysensors/archive/35b87d880147a34107da0d40cb815d75e6cb4af7.zip#pymysensors==0.2
|
||||
|
||||
# Netgear (device_tracker.netgear)
|
||||
|
@ -101,18 +101,18 @@ slacker==0.6.8
|
|||
# Temper sensors (sensor.temper)
|
||||
https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip#temperusb==1.2.3
|
||||
|
||||
# PyEdimax
|
||||
# PyEdimax (switch.edimax)
|
||||
https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1
|
||||
|
||||
# RPI-GPIO platform (*.rpi_gpio)
|
||||
# Uncomment for Raspberry Pi
|
||||
# RPi.GPIO==0.5.11
|
||||
|
||||
# Adafruit temperature/humidity sensor
|
||||
# uncomment on a Raspberry Pi / Beaglebone
|
||||
# Adafruit temperature/humidity sensor (sensor.dht)
|
||||
# Uncomment on a Raspberry Pi / Beaglebone
|
||||
# http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip#Adafruit_DHT==1.1.0
|
||||
|
||||
# PAHO MQTT Binding (mqtt)
|
||||
# PAHO MQTT (mqtt)
|
||||
paho-mqtt==1.1
|
||||
|
||||
# PyModbus (modbus)
|
||||
|
@ -121,19 +121,26 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6
|
|||
# Verisure (verisure)
|
||||
https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6
|
||||
|
||||
# Python tools for interacting with IFTTT Maker Channel (ifttt)
|
||||
# IFTTT Maker Channel (ifttt)
|
||||
pyfttt==0.3
|
||||
|
||||
# sensor.sabnzbd
|
||||
# SABnzbd (sensor.sabnzbd)
|
||||
https://github.com/balloob/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1
|
||||
|
||||
# switch.vera
|
||||
# sensor.vera
|
||||
# light.vera
|
||||
# Vera (*.vera)
|
||||
https://github.com/balloob/home-assistant-vera-api/archive/a8f823066ead6c7da6fb5e7abaf16fef62e63364.zip#python-vera==0.1
|
||||
|
||||
# Sonos bindings (media_player.sonos)
|
||||
# Sonos (media_player.sonos)
|
||||
SoCo==0.11.1
|
||||
|
||||
# PlexAPI (media_player.plex)
|
||||
https://github.com/adrienbrault/python-plexapi/archive/df2d0847e801d6d5cda920326d693cf75f304f1a.zip#python-plexapi==1.0.2
|
||||
|
||||
# SNMP (device_tracker.snmp)
|
||||
pysnmp==4.2.5
|
||||
|
||||
# Blinkstick (light.blinksticklight)
|
||||
blinkstick==1.1.7
|
||||
|
||||
# Telegram (notify.telegram)
|
||||
python-telegram-bot==2.8.7
|
||||
|
|
13
setup.py
13
setup.py
|
@ -9,11 +9,12 @@ DOWNLOAD_URL = ('https://github.com/balloob/home-assistant/archive/'
|
|||
|
||||
PACKAGES = find_packages(exclude=['tests', 'tests.*'])
|
||||
|
||||
PACKAGE_DATA = \
|
||||
{'homeassistant.components.frontend': ['index.html.template'],
|
||||
'homeassistant.components.frontend.www_static': ['*.*'],
|
||||
'homeassistant.components.frontend.www_static.images': ['*.*'],
|
||||
'homeassistant.startup': ['*.*']}
|
||||
# PACKAGE_DATA = \
|
||||
# {'homeassistant.components.frontend': ['index.html.template'],
|
||||
# 'homeassistant.components.frontend.www_static': ['*.*'],
|
||||
# 'homeassistant.components.frontend.www_static.images': ['*.*'],
|
||||
# 'homeassistant.components.mqtt': ['*.crt'],
|
||||
# 'homeassistant.startup': ['*.*']}
|
||||
|
||||
REQUIRES = [
|
||||
'requests>=2,<3',
|
||||
|
@ -23,6 +24,7 @@ REQUIRES = [
|
|||
'vincenty==0.1.2'
|
||||
]
|
||||
|
||||
# package_data=PACKAGE_DATA,
|
||||
setup(
|
||||
name=PACKAGE_NAME,
|
||||
version=__version__,
|
||||
|
@ -34,7 +36,6 @@ setup(
|
|||
description='Open-source home automation platform running on Python 3.',
|
||||
packages=PACKAGES,
|
||||
include_package_data=True,
|
||||
package_data=PACKAGE_DATA,
|
||||
zip_safe=False,
|
||||
platforms='any',
|
||||
install_requires=REQUIRES,
|
||||
|
|
|
@ -152,9 +152,13 @@ class TestLight(unittest.TestCase):
|
|||
data)
|
||||
|
||||
method, data = dev2.last_call('turn_on')
|
||||
self.assertEqual(
|
||||
{light.ATTR_XY_COLOR: color_util.color_RGB_to_xy(255, 255, 255)},
|
||||
data)
|
||||
self.assertEquals(
|
||||
data[light.ATTR_XY_COLOR],
|
||||
color_util.color_RGB_to_xy(255, 255, 255))
|
||||
|
||||
self.assertEquals(
|
||||
data[light.ATTR_RGB_COLOR],
|
||||
[255, 255, 255])
|
||||
|
||||
method, data = dev3.last_call('turn_on')
|
||||
self.assertEqual({light.ATTR_XY_COLOR: [.4, .6]}, data)
|
||||
|
|
|
@ -40,7 +40,7 @@ class TestMediaPlayer(unittest.TestCase):
|
|||
|
||||
def test_services(self):
|
||||
"""
|
||||
Test if the call service methods conver to correct service calls.
|
||||
Test if the call service methods convert to correct service calls.
|
||||
"""
|
||||
services = {
|
||||
SERVICE_TURN_ON: media_player.turn_on,
|
||||
|
|
|
@ -218,3 +218,14 @@ class TestUtil(unittest.TestCase):
|
|||
|
||||
self.assertEqual(3, len(calls1))
|
||||
self.assertEqual(2, len(calls2))
|
||||
|
||||
def test_throttle_per_instance(self):
|
||||
""" Test that the throttle method is done per instance of a class. """
|
||||
|
||||
class Tester(object):
|
||||
@util.Throttle(timedelta(seconds=1))
|
||||
def hello(self):
|
||||
return True
|
||||
|
||||
self.assertTrue(Tester().hello())
|
||||
self.assertTrue(Tester().hello())
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue