Merge pull request #5245 from home-assistant/dev

0.36
This commit is contained in:
Paulus Schoutsen 2017-01-15 08:36:53 -08:00 committed by GitHub
commit 36da5d9adb
382 changed files with 13644 additions and 2229 deletions

View File

@ -13,6 +13,9 @@ omit =
homeassistant/components/arduino.py homeassistant/components/arduino.py
homeassistant/components/*/arduino.py homeassistant/components/*/arduino.py
homeassistant/components/bbb_gpio.py
homeassistant/components/*/bbb_gpio.py
homeassistant/components/bloomsky.py homeassistant/components/bloomsky.py
homeassistant/components/*/bloomsky.py homeassistant/components/*/bloomsky.py
@ -34,6 +37,9 @@ omit =
homeassistant/components/insteon_hub.py homeassistant/components/insteon_hub.py
homeassistant/components/*/insteon_hub.py homeassistant/components/*/insteon_hub.py
homeassistant/components/insteon_local.py
homeassistant/components/*/insteon_local.py
homeassistant/components/ios.py homeassistant/components/ios.py
homeassistant/components/*/ios.py homeassistant/components/*/ios.py
@ -157,13 +163,16 @@ omit =
homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/device_tracker/ping.py
homeassistant/components/device_tracker/snmp.py homeassistant/components/device_tracker/snmp.py
homeassistant/components/device_tracker/swisscom.py homeassistant/components/device_tracker/swisscom.py
homeassistant/components/device_tracker/thomson.py homeassistant/components/device_tracker/thomson.py
homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/tplink.py homeassistant/components/device_tracker/tplink.py
homeassistant/components/device_tracker/trackr.py
homeassistant/components/device_tracker/ubus.py homeassistant/components/device_tracker/ubus.py
homeassistant/components/device_tracker/volvooncall.py homeassistant/components/device_tracker/volvooncall.py
homeassistant/components/device_tracker/xiaomi.py
homeassistant/components/discovery.py homeassistant/components/discovery.py
homeassistant/components/downloader.py homeassistant/components/downloader.py
homeassistant/components/emoncms_history.py homeassistant/components/emoncms_history.py
@ -183,8 +192,10 @@ omit =
homeassistant/components/light/lifx.py homeassistant/components/light/lifx.py
homeassistant/components/light/limitlessled.py homeassistant/components/light/limitlessled.py
homeassistant/components/light/osramlightify.py homeassistant/components/light/osramlightify.py
homeassistant/components/light/tikteck.py
homeassistant/components/light/x10.py homeassistant/components/light/x10.py
homeassistant/components/light/yeelight.py homeassistant/components/light/yeelight.py
homeassistant/components/light/zengge.py
homeassistant/components/lirc.py homeassistant/components/lirc.py
homeassistant/components/media_player/aquostv.py homeassistant/components/media_player/aquostv.py
homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/braviatv.py
@ -202,6 +213,7 @@ omit =
homeassistant/components/media_player/lg_netcast.py homeassistant/components/media_player/lg_netcast.py
homeassistant/components/media_player/mpchc.py homeassistant/components/media_player/mpchc.py
homeassistant/components/media_player/mpd.py homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/nad.py
homeassistant/components/media_player/onkyo.py homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/panasonic_viera.py homeassistant/components/media_player/panasonic_viera.py
homeassistant/components/media_player/pandora.py homeassistant/components/media_player/pandora.py
@ -219,12 +231,14 @@ omit =
homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_lambda.py
homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sns.py
homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/facebook.py
homeassistant/components/notify/free_mobile.py homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py homeassistant/components/notify/gntp.py
homeassistant/components/notify/group.py homeassistant/components/notify/group.py
homeassistant/components/notify/instapush.py homeassistant/components/notify/instapush.py
homeassistant/components/notify/joaoapps_join.py homeassistant/components/notify/joaoapps_join.py
homeassistant/components/notify/kodi.py homeassistant/components/notify/kodi.py
homeassistant/components/notify/lannouncer.py
homeassistant/components/notify/llamalab_automate.py homeassistant/components/notify/llamalab_automate.py
homeassistant/components/notify/matrix.py homeassistant/components/notify/matrix.py
homeassistant/components/notify/message_bird.py homeassistant/components/notify/message_bird.py
@ -254,6 +268,7 @@ omit =
homeassistant/components/sensor/bitcoin.py homeassistant/components/sensor/bitcoin.py
homeassistant/components/sensor/bom.py homeassistant/components/sensor/bom.py
homeassistant/components/sensor/broadlink.py homeassistant/components/sensor/broadlink.py
homeassistant/components/sensor/dublin_bus_transport.py
homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/coinmarketcap.py
homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/cpuspeed.py
homeassistant/components/sensor/cups.py homeassistant/components/sensor/cups.py
@ -277,6 +292,8 @@ omit =
homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hddtemp.py homeassistant/components/sensor/hddtemp.py
homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/hydroquebec.py
homeassistant/components/sensor/iss.py
homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/influxdb.py
@ -301,6 +318,7 @@ omit =
homeassistant/components/sensor/scrape.py homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/sensehat.py homeassistant/components/sensor/sensehat.py
homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/sma.py
homeassistant/components/sensor/snmp.py homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/sonarr.py homeassistant/components/sensor/sonarr.py
homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/speedtest.py
@ -317,6 +335,7 @@ omit =
homeassistant/components/sensor/transmission.py homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/twitch.py homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py homeassistant/components/sensor/uber.py
homeassistant/components/sensor/usps.py
homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/waqi.py homeassistant/components/sensor/waqi.py
homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/xbox_live.py
@ -331,6 +350,7 @@ omit =
homeassistant/components/switch/edimax.py homeassistant/components/switch/edimax.py
homeassistant/components/switch/hikvisioncam.py homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/hook.py homeassistant/components/switch/hook.py
homeassistant/components/switch/kankun.py
homeassistant/components/switch/mystrom.py homeassistant/components/switch/mystrom.py
homeassistant/components/switch/netio.py homeassistant/components/switch/netio.py
homeassistant/components/switch/orvibo.py homeassistant/components/switch/orvibo.py
@ -342,7 +362,9 @@ omit =
homeassistant/components/switch/transmission.py homeassistant/components/switch/transmission.py
homeassistant/components/switch/wake_on_lan.py homeassistant/components/switch/wake_on_lan.py
homeassistant/components/thingspeak.py homeassistant/components/thingspeak.py
homeassistant/components/tts/picotts.py
homeassistant/components/upnp.py homeassistant/components/upnp.py
homeassistant/components/weather/bom.py
homeassistant/components/weather/openweathermap.py homeassistant/components/weather/openweathermap.py
homeassistant/components/zeroconf.py homeassistant/components/zeroconf.py

View File

@ -12,6 +12,8 @@ matrix:
env: TOXENV=typing env: TOXENV=typing
- python: "3.5" - python: "3.5"
env: TOXENV=py35 env: TOXENV=py35
- python: "3.6"
env: TOXENV=py36
allow_failures: allow_failures:
- python: "3.5" - python: "3.5"
env: TOXENV=typing env: TOXENV=typing

View File

@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot
The process is straight-forward. The process is straight-forward.
- Read [How to get faster PR reviews](https://github.com/kubernetes/kubernetes/blob/master/docs/devel/faster_reviews.md) by Kubernetes (but skip step 0) - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/devel/faster_reviews.md) by Kubernetes (but skip step 0)
- Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant). - Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant).
- Write the code for your device, notification service, sensor, or IoT thing. - Write the code for your device, notification service, sensor, or IoT thing.
- Ensure tests work. - Ensure tests work.

View File

@ -6,24 +6,14 @@ VOLUME /config
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN pip3 install --no-cache-dir colorlog cython # Copy build scripts
COPY script/setup_docker_prereqs script/build_python_openzwave script/build_libcec script/
# For the nmap tracker, bluetooth tracker, Z-Wave, tellstick RUN script/setup_docker_prereqs
RUN echo "deb http://download.telldus.com/debian/ stable main" >> /etc/apt/sources.list.d/telldus.list && \
wget -qO - http://download.telldus.se/debian/telldus-public.key | apt-key add - && \
apt-get update && \
apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev bluetooth libbluetooth-dev \
libtelldus-core2 && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY script/build_python_openzwave script/build_python_openzwave
RUN script/build_python_openzwave && \
mkdir -p /usr/local/share/python-openzwave && \
ln -sf /usr/src/app/build/python-openzwave/openzwave/config /usr/local/share/python-openzwave/config
# Install hass component dependencies
COPY requirements_all.txt requirements_all.txt COPY requirements_all.txt requirements_all.txt
RUN pip3 install --no-cache-dir -r requirements_all.txt && \ RUN pip3 install --no-cache-dir -r requirements_all.txt && \
pip3 install mysqlclient psycopg2 uvloop pip3 install --no-cache-dir mysqlclient psycopg2 uvloop
# Copy source # Copy source
COPY . . COPY . .

View File

@ -26,7 +26,8 @@ Examples of devices Home Assistant can interface with:
`Netgear <http://netgear.com>`__, `Netgear <http://netgear.com>`__,
`DD-WRT <http://www.dd-wrt.com/site/index>`__, `DD-WRT <http://www.dd-wrt.com/site/index>`__,
`TPLink <http://www.tp-link.us/>`__, `TPLink <http://www.tp-link.us/>`__,
`ASUSWRT <http://event.asus.com/2013/nw/ASUSWRT/>`__ and any SNMP `ASUSWRT <http://event.asus.com/2013/nw/ASUSWRT/>`__,
`Xiaomi <http://miwifi.com/>`__ and any SNMP
capable Linksys WAP/WRT capable Linksys WAP/WRT
- `Philips Hue <http://meethue.com>`__ lights, - `Philips Hue <http://meethue.com>`__ lights,
`WeMo <http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/>`__ `WeMo <http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/>`__

View File

@ -356,7 +356,8 @@ def try_to_restart() -> None:
def main() -> int: def main() -> int:
"""Start Home Assistant.""" """Start Home Assistant."""
monkey_patch_asyncio() if sys.version_info[:3] < (3, 5, 3):
monkey_patch_asyncio()
validate_python() validate_python()

View File

@ -395,6 +395,10 @@ def async_from_config_dict(config: Dict[str, Any],
if not loader.PREPARED: if not loader.PREPARED:
yield from hass.loop.run_in_executor(None, loader.prepare, hass) yield from hass.loop.run_in_executor(None, loader.prepare, hass)
# Merge packages
conf_util.merge_packages_config(
config, core_config.get(conf_util.CONF_PACKAGES, {}))
# Make a copy because we are mutating it. # Make a copy because we are mutating it.
# Use OrderedDict in case original one was one. # Use OrderedDict in case original one was one.
# Convert values to dictionaries if they are None # Convert values to dictionaries if they are None
@ -606,7 +610,7 @@ def async_log_exception(ex, domain, config, hass):
message += '{}.'.format(humanize_error(config, ex)) message += '{}.'.format(humanize_error(config, ex))
domain_config = config.get(domain, config) domain_config = config.get(domain, config)
message += " (See {}:{}). ".format( message += " (See {}, line {}). ".format(
getattr(domain_config, '__config_file__', '?'), getattr(domain_config, '__config_file__', '?'),
getattr(domain_config, '__line__', '?')) getattr(domain_config, '__line__', '?'))

View File

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel/ https://home-assistant.io/components/alarm_control_panel/
""" """
import asyncio import asyncio
from datetime import timedelta
import logging import logging
import os import os
@ -20,7 +21,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
DOMAIN = 'alarm_control_panel' DOMAIN = 'alarm_control_panel'
SCAN_INTERVAL = 30 SCAN_INTERVAL = timedelta(seconds=30)
ATTR_CHANGED_BY = 'changed_by' ATTR_CHANGED_BY = 'changed_by'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -152,40 +153,48 @@ class AlarmControlPanel(Entity):
"""Send disarm command.""" """Send disarm command."""
raise NotImplementedError() raise NotImplementedError()
@asyncio.coroutine
def async_alarm_disarm(self, code=None): def async_alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command.
yield from self.hass.loop.run_in_executor(
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.alarm_disarm, code) None, self.alarm_disarm, code)
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
raise NotImplementedError() raise NotImplementedError()
@asyncio.coroutine
def async_alarm_arm_home(self, code=None): def async_alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command.
yield from self.hass.loop.run_in_executor(
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.alarm_arm_home, code) None, self.alarm_arm_home, code)
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
raise NotImplementedError() raise NotImplementedError()
@asyncio.coroutine
def async_alarm_arm_away(self, code=None): def async_alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command.
yield from self.hass.loop.run_in_executor(
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.alarm_arm_away, code) None, self.alarm_arm_away, code)
def alarm_trigger(self, code=None): def alarm_trigger(self, code=None):
"""Send alarm trigger command.""" """Send alarm trigger command."""
raise NotImplementedError() raise NotImplementedError()
@asyncio.coroutine
def async_alarm_trigger(self, code=None): def async_alarm_trigger(self, code=None):
"""Send alarm trigger command.""" """Send alarm trigger command.
yield from self.hass.loop.run_in_executor(
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.alarm_trigger, code) None, self.alarm_trigger, code)
@property @property

View File

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.concord232/ https://home-assistant.io/components/alarm_control_panel.concord232/
""" """
import datetime import datetime
from datetime import timedelta
import logging import logging
import requests import requests
@ -25,7 +26,7 @@ DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'CONCORD232' DEFAULT_NAME = 'CONCORD232'
DEFAULT_PORT = 5007 DEFAULT_PORT = 5007
SCAN_INTERVAL = 1 SCAN_INTERVAL = timedelta(seconds=1)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,

View File

@ -16,7 +16,7 @@ from homeassistant.const import (
STATE_UNKNOWN, CONF_NAME, CONF_HOST, CONF_PORT) STATE_UNKNOWN, CONF_NAME, CONF_HOST, CONF_PORT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pynx584==0.2'] REQUIREMENTS = ['pynx584==0.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -86,9 +86,11 @@ class NX584Alarm(alarm.AlarmControlPanel):
_LOGGER.error('Unable to connect to %(host)s: %(reason)s', _LOGGER.error('Unable to connect to %(host)s: %(reason)s',
dict(host=self._url, reason=ex)) dict(host=self._url, reason=ex))
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
zones = []
except IndexError: except IndexError:
_LOGGER.error('nx584 reports no partitions') _LOGGER.error('nx584 reports no partitions')
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
zones = []
bypassed = False bypassed = False
for zone in zones: for zone in zones:

View File

@ -203,11 +203,12 @@ class AlexaResponse(object):
self.reprompt = None self.reprompt = None
self.session_attributes = {} self.session_attributes = {}
self.should_end_session = True self.should_end_session = True
self.variables = {}
if intent is not None and 'slots' in intent: if intent is not None and 'slots' in intent:
self.variables = {key: value['value'] for key, value for key, value in intent['slots'].items():
in intent['slots'].items() if 'value' in value} if 'value' in value:
else: underscored_key = key.replace('.', '_')
self.variables = {} self.variables[underscored_key] = value['value']
def add_card(self, card_type, title, content): def add_card(self, card_type, title, content):
"""Add a card to the response.""" """Add a card to the response."""

View File

@ -133,6 +133,9 @@ class APIEventStream(HomeAssistantView):
except asyncio.TimeoutError: except asyncio.TimeoutError:
yield from to_write.put(STREAM_PING_PAYLOAD) yield from to_write.put(STREAM_PING_PAYLOAD)
except asyncio.CancelledError:
_LOGGER.debug('STREAM %s ABORT', id(stop_obj))
finally: finally:
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
unsub_stream() unsub_stream()

View File

@ -0,0 +1,74 @@
"""
Support for controlling GPIO pins of a Beaglebone Black.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/bbb_gpio/
"""
import logging
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = ['Adafruit_BBIO==1.0.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'bbb_gpio'
# pylint: disable=no-member
def setup(hass, config):
"""Set up the BeagleBone Black GPIO component."""
# pylint: disable=import-error
import Adafruit_BBIO.GPIO as GPIO
def cleanup_gpio(event):
"""Stuff to do before stopping."""
GPIO.cleanup()
def prepare_gpio(event):
"""Stuff to do when home assistant starts."""
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio)
return True
# noqa: F821
def setup_output(pin):
"""Setup a GPIO as output."""
# pylint: disable=import-error,undefined-variable
import Adafruit_BBIO.GPIO as GPIO
GPIO.setup(pin, GPIO.OUT)
def setup_input(pin, pull_mode):
"""Setup a GPIO as input."""
# pylint: disable=import-error,undefined-variable
import Adafruit_BBIO.GPIO as GPIO
GPIO.setup(pin, GPIO.IN, # noqa: F821
GPIO.PUD_DOWN if pull_mode == 'DOWN' # noqa: F821
else GPIO.PUD_UP) # noqa: F821
def write_output(pin, value):
"""Write a value to a GPIO."""
# pylint: disable=import-error,undefined-variable
import Adafruit_BBIO.GPIO as GPIO
GPIO.output(pin, value)
def read_input(pin):
"""Read a value from a GPIO."""
# pylint: disable=import-error,undefined-variable
import Adafruit_BBIO.GPIO as GPIO
return GPIO.input(pin)
def edge_detect(pin, event_callback, bounce):
"""Add detection for RISING and FALLING events."""
# pylint: disable=import-error,undefined-variable
import Adafruit_BBIO.GPIO as GPIO
GPIO.add_event_detect(
pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)

View File

@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor/ https://home-assistant.io/components/binary_sensor/
""" """
import asyncio import asyncio
from datetime import timedelta
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -15,7 +16,7 @@ from homeassistant.const import (STATE_ON, STATE_OFF)
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
DOMAIN = 'binary_sensor' DOMAIN = 'binary_sensor'
SCAN_INTERVAL = 30 SCAN_INTERVAL = timedelta(seconds=30)
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
SENSOR_CLASSES = [ SENSOR_CLASSES = [

View File

@ -4,6 +4,7 @@ Support for custom shell commands to retrieve values.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.command_line/ https://home-assistant.io/components/binary_sensor.command_line/
""" """
from datetime import timedelta
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -22,7 +23,7 @@ DEFAULT_NAME = 'Binary Command Sensor'
DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_PAYLOAD_OFF = 'OFF'
SCAN_INTERVAL = 60 SCAN_INTERVAL = timedelta(seconds=60)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COMMAND): cv.string, vol.Required(CONF_COMMAND): cv.string,

View File

@ -27,7 +27,7 @@ DEFAULT_NAME = 'Alarm'
DEFAULT_PORT = '5007' DEFAULT_PORT = '5007'
DEFAULT_SSL = False DEFAULT_SSL = False
SCAN_INTERVAL = 1 SCAN_INTERVAL = datetime.timedelta(seconds=1)
ZONE_TYPES_SCHEMA = vol.Schema({ ZONE_TYPES_SCHEMA = vol.Schema({
cv.positive_int: vol.In(SENSOR_CLASSES), cv.positive_int: vol.In(SENSOR_CLASSES),

View File

@ -1,6 +1,6 @@
"""Contains functionality to use flic buttons as a binary sensor.""" """Contains functionality to use flic buttons as a binary sensor."""
import asyncio
import logging import logging
import threading
import voluptuous as vol import voluptuous as vol
@ -10,7 +10,6 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP) EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.util.async import run_callback_threadsafe
REQUIREMENTS = ['https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4'] REQUIREMENTS = ['https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4']
@ -43,9 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine def setup_platform(hass, config, add_entities, discovery_info=None):
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Setup the flic platform.""" """Setup the flic platform."""
import pyflic import pyflic
@ -63,26 +60,29 @@ def async_setup_platform(hass, config, async_add_entities,
def new_button_callback(address): def new_button_callback(address):
"""Setup newly verified button as device in home assistant.""" """Setup newly verified button as device in home assistant."""
hass.add_job(async_setup_button(hass, config, async_add_entities, setup_button(hass, config, add_entities, client, address)
client, address))
client.on_new_verified_button = new_button_callback client.on_new_verified_button = new_button_callback
if discovery: if discovery:
start_scanning(hass, config, async_add_entities, client) start_scanning(config, add_entities, client)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: client.close()) lambda event: client.close())
hass.loop.run_in_executor(None, client.handle_events)
# Start the pyflic event handling thread
threading.Thread(target=client.handle_events).start()
def get_info_callback(items):
"""Add entities for already verified buttons."""
addresses = items["bd_addr_of_verified_buttons"] or []
for address in addresses:
setup_button(hass, config, add_entities, client, address)
# Get addresses of already verified buttons # Get addresses of already verified buttons
addresses = yield from async_get_verified_addresses(client) client.get_info(get_info_callback)
if addresses:
for address in addresses:
yield from async_setup_button(hass, config, async_add_entities,
client, address)
def start_scanning(hass, config, async_add_entities, client): def start_scanning(config, add_entities, client):
"""Start a new flic client for scanning & connceting to new buttons.""" """Start a new flic client for scanning & connceting to new buttons."""
import pyflic import pyflic
@ -97,36 +97,20 @@ def start_scanning(hass, config, async_add_entities, client):
address, result) address, result)
# Restart scan wizard # Restart scan wizard
start_scanning(hass, config, async_add_entities, client) start_scanning(config, add_entities, client)
scan_wizard.on_completed = scan_completed_callback scan_wizard.on_completed = scan_completed_callback
client.add_scan_wizard(scan_wizard) client.add_scan_wizard(scan_wizard)
@asyncio.coroutine def setup_button(hass, config, add_entities, client, address):
def async_setup_button(hass, config, async_add_entities, client, address):
"""Setup single button device.""" """Setup single button device."""
timeout = config.get(CONF_TIMEOUT) timeout = config.get(CONF_TIMEOUT)
ignored_click_types = config.get(CONF_IGNORED_CLICK_TYPES) ignored_click_types = config.get(CONF_IGNORED_CLICK_TYPES)
button = FlicButton(hass, client, address, timeout, ignored_click_types) button = FlicButton(hass, client, address, timeout, ignored_click_types)
_LOGGER.info("Connected to button (%s)", address) _LOGGER.info("Connected to button (%s)", address)
yield from async_add_entities([button]) add_entities([button])
@asyncio.coroutine
def async_get_verified_addresses(client):
"""Retrieve addresses of verified buttons."""
future = asyncio.Future()
loop = asyncio.get_event_loop()
def get_info_callback(items):
"""Set the addressed of connected buttons as result of the future."""
addresses = items["bd_addr_of_verified_buttons"]
run_callback_threadsafe(loop, future.set_result, addresses)
client.get_info(get_info_callback)
return future
class FlicButton(BinarySensorDevice): class FlicButton(BinarySensorDevice):

View File

@ -17,7 +17,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_SSL, EVENT_HOMEASSISTANT_STOP, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE) CONF_SSL, EVENT_HOMEASSISTANT_STOP, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
REQUIREMENTS = ['pyhik==0.0.6', 'pydispatcher==2.0.5'] REQUIREMENTS = ['pyhik==0.0.7', 'pydispatcher==2.0.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_IGNORED = 'ignored' CONF_IGNORED = 'ignored'

View File

@ -47,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
devices = {} devices = {}
gateway.platform_callbacks.append(mysensors.pf_callback_factory( gateway.platform_callbacks.append(mysensors.pf_callback_factory(
map_sv_types, devices, add_devices, MySensorsBinarySensor)) map_sv_types, devices, MySensorsBinarySensor, add_devices))
class MySensorsBinarySensor( class MySensorsBinarySensor(

View File

@ -16,7 +16,7 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import (CONF_HOST, CONF_PORT) from homeassistant.const import (CONF_HOST, CONF_PORT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pynx584==0.2'] REQUIREMENTS = ['pynx584==0.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -20,6 +20,8 @@ DEPENDENCIES = []
PHILIO = 0x013c PHILIO = 0x013c
PHILIO_SLIM_SENSOR = 0x0002 PHILIO_SLIM_SENSOR = 0x0002
PHILIO_SLIM_SENSOR_MOTION = (PHILIO, PHILIO_SLIM_SENSOR, 0) PHILIO_SLIM_SENSOR_MOTION = (PHILIO, PHILIO_SLIM_SENSOR, 0)
PHILIO_3_IN_1_SENSOR_GEN_4 = 0x000d
PHILIO_3_IN_1_SENSOR_GEN_4_MOTION = (PHILIO, PHILIO_3_IN_1_SENSOR_GEN_4, 0)
WENZHOU = 0x0118 WENZHOU = 0x0118
WENZHOU_SLIM_SENSOR_MOTION = (WENZHOU, PHILIO_SLIM_SENSOR, 0) WENZHOU_SLIM_SENSOR_MOTION = (WENZHOU, PHILIO_SLIM_SENSOR, 0)
@ -27,6 +29,7 @@ WORKAROUND_NO_OFF_EVENT = 'trigger_no_off_event'
DEVICE_MAPPINGS = { DEVICE_MAPPINGS = {
PHILIO_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT, PHILIO_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT,
PHILIO_3_IN_1_SENSOR_GEN_4_MOTION: WORKAROUND_NO_OFF_EVENT,
WENZHOU_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT, WENZHOU_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT,
} }
@ -96,6 +99,7 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
"""Called when a value has changed on the network.""" """Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \ if self._value.value_id == value.value_id or \
self._value.node == value.node: self._value.node == value.node:
_LOGGER.debug('Value changed for label %s', self._value.label)
self.schedule_update_ha_state() self.schedule_update_ha_state()

View File

@ -6,6 +6,8 @@ https://home-assistant.io/components/calendar/
""" """
import logging import logging
from datetime import timedelta
import re import re
from homeassistant.components.google import (CONF_OFFSET, from homeassistant.components.google import (CONF_OFFSET,
@ -20,6 +22,7 @@ from homeassistant.util import dt
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=60)
DOMAIN = 'calendar' DOMAIN = 'calendar'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -27,7 +30,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
def setup(hass, config): def setup(hass, config):
"""Track states and offer events for calendars.""" """Track states and offer events for calendars."""
component = EntityComponent( component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, 60, DOMAIN) logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, DOMAIN)
component.setup(config) component.setup(config)
@ -144,10 +147,10 @@ class CalendarEventDevice(Entity):
def _get_date(date): def _get_date(date):
"""Get the dateTime from date or dateTime as a local.""" """Get the dateTime from date or dateTime as a local."""
if 'date' in date: if 'date' in date:
return dt.as_utc(dt.dt.datetime.combine( return dt.start_of_local_day(dt.dt.datetime.combine(
dt.parse_date(date['date']), dt.dt.time())) dt.parse_date(date['date']), dt.dt.time.min))
else: else:
return dt.parse_datetime(date['dateTime']) return dt.as_local(dt.parse_datetime(date['dateTime']))
start = _get_date(self.data.event['start']) start = _get_date(self.data.event['start'])
end = _get_date(self.data.event['end']) end = _get_date(self.data.event['end'])

View File

@ -66,7 +66,7 @@ class GoogleCalendarData(object):
"""Get the latest data.""" """Get the latest data."""
service = self.calendar_service.get() service = self.calendar_service.get()
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
params['timeMin'] = dt.utcnow().isoformat('T') params['timeMin'] = dt.start_of_local_day().isoformat('T')
params['calendarId'] = self.calendar_id params['calendarId'] = self.calendar_id
if self.search: if self.search:
params['q'] = self.search params['q'] = self.search

View File

@ -6,18 +6,27 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/camera/ https://home-assistant.io/components/camera/
""" """
import asyncio import asyncio
from datetime import timedelta
import logging import logging
import hashlib
import aiohttp
from aiohttp import web from aiohttp import web
import async_timeout
from homeassistant.const import ATTR_ENTITY_PICTURE
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'camera' DOMAIN = 'camera'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
SCAN_INTERVAL = 30 SCAN_INTERVAL = timedelta(seconds=30)
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
STATE_RECORDING = 'recording' STATE_RECORDING = 'recording'
@ -27,11 +36,45 @@ STATE_IDLE = 'idle'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
@asyncio.coroutine
def async_get_image(hass, entity_id, timeout=10):
"""Fetch a image from a camera entity."""
websession = async_get_clientsession(hass)
state = hass.states.get(entity_id)
if state is None:
raise HomeAssistantError(
"No entity '{0}' for grab a image".format(entity_id))
url = "{0}{1}".format(
hass.config.api.base_url,
state.attributes.get(ATTR_ENTITY_PICTURE)
)
response = None
try:
with async_timeout.timeout(timeout, loop=hass.loop):
response = yield from websession.get(url)
if response.status != 200:
raise HomeAssistantError("Error {0} on {1}".format(
response.status, url))
image = yield from response.read()
return image
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
raise HomeAssistantError("Can't connect to {0}".format(url))
finally:
if response is not None:
yield from response.release()
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Setup the camera component.""" """Setup the camera component."""
component = EntityComponent( component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
hass.http.register_view(CameraImageView(component.entities)) hass.http.register_view(CameraImageView(component.entities))
hass.http.register_view(CameraMjpegStream(component.entities)) hass.http.register_view(CameraMjpegStream(component.entities))
@ -46,11 +89,13 @@ class Camera(Entity):
def __init__(self): def __init__(self):
"""Initialize a camera.""" """Initialize a camera."""
self.is_streaming = False self.is_streaming = False
self._access_token = hashlib.sha256(
str.encode(str(id(self)))).hexdigest()
@property @property
def access_token(self): def access_token(self):
"""Access token for this camera.""" """Access token for this camera."""
return str(id(self)) return self._access_token
@property @property
def should_poll(self): def should_poll(self):
@ -81,15 +126,12 @@ class Camera(Entity):
"""Return bytes of camera image.""" """Return bytes of camera image."""
raise NotImplementedError() raise NotImplementedError()
@asyncio.coroutine
def async_camera_image(self): def async_camera_image(self):
"""Return bytes of camera image. """Return bytes of camera image.
This method must be run in the event loop. This method must be run in the event loop and returns a coroutine.
""" """
image = yield from self.hass.loop.run_in_executor( return self.hass.loop.run_in_executor(None, self.camera_image)
None, self.camera_image)
return image
@asyncio.coroutine @asyncio.coroutine
def handle_async_mjpeg_stream(self, request): def handle_async_mjpeg_stream(self, request):
@ -131,8 +173,14 @@ class Camera(Entity):
yield from response.drain() yield from response.drain()
yield from asyncio.sleep(.5) yield from asyncio.sleep(.5)
except asyncio.CancelledError:
_LOGGER.debug("Close stream by frontend.")
response = None
finally: finally:
yield from response.write_eof() if response is not None:
yield from response.write_eof()
@property @property
def state(self): def state(self):
@ -201,12 +249,16 @@ class CameraImageView(CameraView):
@asyncio.coroutine @asyncio.coroutine
def handle(self, request, camera): def handle(self, request, camera):
"""Serve camera image.""" """Serve camera image."""
image = yield from camera.async_camera_image() try:
image = yield from camera.async_camera_image()
if image is None: if image is None:
return web.Response(status=500) return web.Response(status=500)
return web.Response(body=image) return web.Response(body=image)
except asyncio.CancelledError:
_LOGGER.debug("Close stream by frontend.")
class CameraMjpegStream(CameraView): class CameraMjpegStream(CameraView):

View File

@ -84,9 +84,15 @@ class FFmpegCamera(Camera):
if not data: if not data:
break break
response.write(data) response.write(data)
except asyncio.CancelledError:
_LOGGER.debug("Close stream by frontend.")
response = None
finally: finally:
self.hass.async_add_job(stream.close()) yield from stream.close()
yield from response.write_eof() if response is not None:
yield from response.write_eof()
@property @property
def name(self): def name(self):

View File

@ -124,9 +124,13 @@ class MjpegCamera(Camera):
except asyncio.TimeoutError: except asyncio.TimeoutError:
raise HTTPGatewayTimeout() raise HTTPGatewayTimeout()
except asyncio.CancelledError:
_LOGGER.debug("Close stream by frontend.")
response = None
finally: finally:
if stream is not None: if stream is not None:
self.hass.async_add_job(stream.release()) stream.close()
if response is not None: if response is not None:
yield from response.write_eof() yield from response.write_eof()

View File

@ -276,9 +276,13 @@ class SynologyCamera(Camera):
_LOGGER.exception("Error on %s", streaming_url) _LOGGER.exception("Error on %s", streaming_url)
raise HTTPGatewayTimeout() raise HTTPGatewayTimeout()
except asyncio.CancelledError:
_LOGGER.debug("Close stream by frontend.")
response = None
finally: finally:
if stream is not None: if stream is not None:
self.hass.async_add_job(stream.release()) stream.close()
if response is not None: if response is not None:
yield from response.write_eof() yield from response.write_eof()

View File

@ -14,7 +14,7 @@ from homeassistant.const import CONF_PORT
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['uvcclient==0.9.0'] REQUIREMENTS = ['uvcclient==0.10.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -4,15 +4,18 @@ Provides functionality to interact with climate devices.
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/climate/ https://home-assistant.io/components/climate/
""" """
import asyncio
from datetime import timedelta
import logging import logging
import os import os
import functools as ft
from numbers import Number from numbers import Number
import voluptuous as vol
from homeassistant.helpers.entity_component import EntityComponent import voluptuous as vol
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -23,7 +26,7 @@ from homeassistant.const import (
DOMAIN = "climate" DOMAIN = "climate"
ENTITY_ID_FORMAT = DOMAIN + ".{}" ENTITY_ID_FORMAT = DOMAIN + ".{}"
SCAN_INTERVAL = 60 SCAN_INTERVAL = timedelta(seconds=60)
SERVICE_SET_AWAY_MODE = "set_away_mode" SERVICE_SET_AWAY_MODE = "set_away_mode"
SERVICE_SET_AUX_HEAT = "set_aux_heat" SERVICE_SET_AUX_HEAT = "set_aux_heat"
@ -185,17 +188,38 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
def setup(hass, config): @asyncio.coroutine
def async_setup(hass, config):
"""Setup climate devices.""" """Setup climate devices."""
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
component.setup(config) yield from component.async_setup(config)
descriptions = load_yaml_config_file( descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml')) os.path.join(os.path.dirname(__file__), 'services.yaml'))
def away_mode_set_service(service): @asyncio.coroutine
def _async_update_climate(target_climate):
"""Update climate entity after service stuff."""
update_tasks = []
for climate in target_climate:
if not climate.should_poll:
continue
update_coro = hass.loop.create_task(
climate.async_update_ha_state(True))
if hasattr(climate, 'async_update'):
update_tasks.append(update_coro)
else:
yield from update_coro
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
@asyncio.coroutine
def async_away_mode_set_service(service):
"""Set away mode on target climate devices.""" """Set away mode on target climate devices."""
target_climate = component.extract_from_service(service) target_climate = component.async_extract_from_service(service)
away_mode = service.data.get(ATTR_AWAY_MODE) away_mode = service.data.get(ATTR_AWAY_MODE)
@ -207,21 +231,21 @@ def setup(hass, config):
for climate in target_climate: for climate in target_climate:
if away_mode: if away_mode:
climate.turn_away_mode_on() yield from climate.async_turn_away_mode_on()
else: else:
climate.turn_away_mode_off() yield from climate.async_turn_away_mode_off()
if climate.should_poll: yield from _async_update_climate(target_climate)
climate.update_ha_state(True)
hass.services.register( hass.services.async_register(
DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service, DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service,
descriptions.get(SERVICE_SET_AWAY_MODE), descriptions.get(SERVICE_SET_AWAY_MODE),
schema=SET_AWAY_MODE_SCHEMA) schema=SET_AWAY_MODE_SCHEMA)
def aux_heat_set_service(service): @asyncio.coroutine
def async_aux_heat_set_service(service):
"""Set auxillary heater on target climate devices.""" """Set auxillary heater on target climate devices."""
target_climate = component.extract_from_service(service) target_climate = component.async_extract_from_service(service)
aux_heat = service.data.get(ATTR_AUX_HEAT) aux_heat = service.data.get(ATTR_AUX_HEAT)
@ -233,21 +257,21 @@ def setup(hass, config):
for climate in target_climate: for climate in target_climate:
if aux_heat: if aux_heat:
climate.turn_aux_heat_on() yield from climate.async_turn_aux_heat_on()
else: else:
climate.turn_aux_heat_off() yield from climate.async_turn_aux_heat_off()
if climate.should_poll: yield from _async_update_climate(target_climate)
climate.update_ha_state(True)
hass.services.register( hass.services.async_register(
DOMAIN, SERVICE_SET_AUX_HEAT, aux_heat_set_service, DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service,
descriptions.get(SERVICE_SET_AUX_HEAT), descriptions.get(SERVICE_SET_AUX_HEAT),
schema=SET_AUX_HEAT_SCHEMA) schema=SET_AUX_HEAT_SCHEMA)
def temperature_set_service(service): @asyncio.coroutine
def async_temperature_set_service(service):
"""Set temperature on the target climate devices.""" """Set temperature on the target climate devices."""
target_climate = component.extract_from_service(service) target_climate = component.async_extract_from_service(service)
for climate in target_climate: for climate in target_climate:
kwargs = {} kwargs = {}
@ -261,18 +285,19 @@ def setup(hass, config):
else: else:
kwargs[value] = temp kwargs[value] = temp
climate.set_temperature(**kwargs) yield from climate.async_set_temperature(**kwargs)
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register( yield from _async_update_climate(target_climate)
DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service,
hass.services.async_register(
DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service,
descriptions.get(SERVICE_SET_TEMPERATURE), descriptions.get(SERVICE_SET_TEMPERATURE),
schema=SET_TEMPERATURE_SCHEMA) schema=SET_TEMPERATURE_SCHEMA)
def humidity_set_service(service): @asyncio.coroutine
def async_humidity_set_service(service):
"""Set humidity on the target climate devices.""" """Set humidity on the target climate devices."""
target_climate = component.extract_from_service(service) target_climate = component.async_extract_from_service(service)
humidity = service.data.get(ATTR_HUMIDITY) humidity = service.data.get(ATTR_HUMIDITY)
@ -283,19 +308,19 @@ def setup(hass, config):
return return
for climate in target_climate: for climate in target_climate:
climate.set_humidity(humidity) yield from climate.async_set_humidity(humidity)
if climate.should_poll: yield from _async_update_climate(target_climate)
climate.update_ha_state(True)
hass.services.register( hass.services.async_register(
DOMAIN, SERVICE_SET_HUMIDITY, humidity_set_service, DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service,
descriptions.get(SERVICE_SET_HUMIDITY), descriptions.get(SERVICE_SET_HUMIDITY),
schema=SET_HUMIDITY_SCHEMA) schema=SET_HUMIDITY_SCHEMA)
def fan_mode_set_service(service): @asyncio.coroutine
def async_fan_mode_set_service(service):
"""Set fan mode on target climate devices.""" """Set fan mode on target climate devices."""
target_climate = component.extract_from_service(service) target_climate = component.async_extract_from_service(service)
fan = service.data.get(ATTR_FAN_MODE) fan = service.data.get(ATTR_FAN_MODE)
@ -306,19 +331,19 @@ def setup(hass, config):
return return
for climate in target_climate: for climate in target_climate:
climate.set_fan_mode(fan) yield from climate.async_set_fan_mode(fan)
if climate.should_poll: yield from _async_update_climate(target_climate)
climate.update_ha_state(True)
hass.services.register( hass.services.async_register(
DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service, DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service,
descriptions.get(SERVICE_SET_FAN_MODE), descriptions.get(SERVICE_SET_FAN_MODE),
schema=SET_FAN_MODE_SCHEMA) schema=SET_FAN_MODE_SCHEMA)
def operation_set_service(service): @asyncio.coroutine
def async_operation_set_service(service):
"""Set operating mode on the target climate devices.""" """Set operating mode on the target climate devices."""
target_climate = component.extract_from_service(service) target_climate = component.async_extract_from_service(service)
operation_mode = service.data.get(ATTR_OPERATION_MODE) operation_mode = service.data.get(ATTR_OPERATION_MODE)
@ -329,19 +354,19 @@ def setup(hass, config):
return return
for climate in target_climate: for climate in target_climate:
climate.set_operation_mode(operation_mode) yield from climate.async_set_operation_mode(operation_mode)
if climate.should_poll: yield from _async_update_climate(target_climate)
climate.update_ha_state(True)
hass.services.register( hass.services.async_register(
DOMAIN, SERVICE_SET_OPERATION_MODE, operation_set_service, DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service,
descriptions.get(SERVICE_SET_OPERATION_MODE), descriptions.get(SERVICE_SET_OPERATION_MODE),
schema=SET_OPERATION_MODE_SCHEMA) schema=SET_OPERATION_MODE_SCHEMA)
def swing_set_service(service): @asyncio.coroutine
def async_swing_set_service(service):
"""Set swing mode on the target climate devices.""" """Set swing mode on the target climate devices."""
target_climate = component.extract_from_service(service) target_climate = component.async_extract_from_service(service)
swing_mode = service.data.get(ATTR_SWING_MODE) swing_mode = service.data.get(ATTR_SWING_MODE)
@ -352,15 +377,15 @@ def setup(hass, config):
return return
for climate in target_climate: for climate in target_climate:
climate.set_swing_mode(swing_mode) yield from climate.async_set_swing_mode(swing_mode)
if climate.should_poll: yield from _async_update_climate(target_climate)
climate.update_ha_state(True)
hass.services.register( hass.services.async_register(
DOMAIN, SERVICE_SET_SWING_MODE, swing_set_service, DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service,
descriptions.get(SERVICE_SET_SWING_MODE), descriptions.get(SERVICE_SET_SWING_MODE),
schema=SET_SWING_MODE_SCHEMA) schema=SET_SWING_MODE_SCHEMA)
return True return True
@ -521,38 +546,110 @@ class ClimateDevice(Entity):
"""Set new target temperature.""" """Set new target temperature."""
raise NotImplementedError() raise NotImplementedError()
def async_set_temperature(self, **kwargs):
"""Set new target temperature.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.set_temperature, **kwargs))
def set_humidity(self, humidity): def set_humidity(self, humidity):
"""Set new target humidity.""" """Set new target humidity."""
raise NotImplementedError() raise NotImplementedError()
def async_set_humidity(self, humidity):
"""Set new target humidity.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.set_humidity, humidity)
def set_fan_mode(self, fan): def set_fan_mode(self, fan):
"""Set new target fan mode.""" """Set new target fan mode."""
raise NotImplementedError() raise NotImplementedError()
def async_set_fan_mode(self, fan):
"""Set new target fan mode.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.set_fan_mode, fan)
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set new target operation mode.""" """Set new target operation mode."""
raise NotImplementedError() raise NotImplementedError()
def async_set_operation_mode(self, operation_mode):
"""Set new target operation mode.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.set_operation_mode, operation_mode)
def set_swing_mode(self, swing_mode): def set_swing_mode(self, swing_mode):
"""Set new target swing operation.""" """Set new target swing operation."""
raise NotImplementedError() raise NotImplementedError()
def async_set_swing_mode(self, swing_mode):
"""Set new target swing operation.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.set_swing_mode, swing_mode)
def turn_away_mode_on(self): def turn_away_mode_on(self):
"""Turn away mode on.""" """Turn away mode on."""
raise NotImplementedError() raise NotImplementedError()
def async_turn_away_mode_on(self):
"""Turn away mode on.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_away_mode_on)
def turn_away_mode_off(self): def turn_away_mode_off(self):
"""Turn away mode off.""" """Turn away mode off."""
raise NotImplementedError() raise NotImplementedError()
def async_turn_away_mode_off(self):
"""Turn away mode off.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_away_mode_off)
def turn_aux_heat_on(self): def turn_aux_heat_on(self):
"""Turn auxillary heater on.""" """Turn auxillary heater on."""
raise NotImplementedError() raise NotImplementedError()
def async_turn_aux_heat_on(self):
"""Turn auxillary heater on.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_aux_heat_on)
def turn_aux_heat_off(self): def turn_aux_heat_off(self):
"""Turn auxillary heater off.""" """Turn auxillary heater off."""
raise NotImplementedError() raise NotImplementedError()
def async_turn_aux_heat_off(self):
"""Turn auxillary heater off.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_aux_heat_off)
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""

View File

@ -22,16 +22,25 @@ _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_FAN_MIN_ON_TIME = 'fan_min_on_time' ATTR_FAN_MIN_ON_TIME = 'fan_min_on_time'
ATTR_RESUME_ALL = 'resume_all'
DEFAULT_RESUME_ALL = False
DEPENDENCIES = ['ecobee'] DEPENDENCIES = ['ecobee']
SERVICE_SET_FAN_MIN_ON_TIME = 'ecobee_set_fan_min_on_time' SERVICE_SET_FAN_MIN_ON_TIME = 'ecobee_set_fan_min_on_time'
SERVICE_RESUME_PROGRAM = 'ecobee_resume_program'
SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema({ SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int), vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int),
}) })
RESUME_PROGRAM_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_RESUME_ALL, default=DEFAULT_RESUME_ALL): cv.boolean,
})
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Ecobee Thermostat Platform.""" """Setup the Ecobee Thermostat Platform."""
@ -48,21 +57,36 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
def fan_min_on_time_set_service(service): def fan_min_on_time_set_service(service):
"""Set the minimum fan on time on the target thermostats.""" """Set the minimum fan on time on the target thermostats."""
entity_id = service.data.get('entity_id') entity_id = service.data.get(ATTR_ENTITY_ID)
fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME]
if entity_id: if entity_id:
target_thermostats = [device for device in devices target_thermostats = [device for device in devices
if device.entity_id == entity_id] if device.entity_id in entity_id]
else: else:
target_thermostats = devices target_thermostats = devices
fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME]
for thermostat in target_thermostats: for thermostat in target_thermostats:
thermostat.set_fan_min_on_time(str(fan_min_on_time)) thermostat.set_fan_min_on_time(str(fan_min_on_time))
thermostat.update_ha_state(True) thermostat.update_ha_state(True)
def resume_program_set_service(service):
"""Resume the program on the target thermostats."""
entity_id = service.data.get(ATTR_ENTITY_ID)
resume_all = service.data.get(ATTR_RESUME_ALL)
if entity_id:
target_thermostats = [device for device in devices
if device.entity_id in entity_id]
else:
target_thermostats = devices
for thermostat in target_thermostats:
thermostat.resume_program(resume_all)
thermostat.update_ha_state(True)
descriptions = load_yaml_config_file( descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml')) path.join(path.dirname(__file__), 'services.yaml'))
@ -71,6 +95,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
descriptions.get(SERVICE_SET_FAN_MIN_ON_TIME), descriptions.get(SERVICE_SET_FAN_MIN_ON_TIME),
schema=SET_FAN_MIN_ON_TIME_SCHEMA) schema=SET_FAN_MIN_ON_TIME_SCHEMA)
hass.services.register(
DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service,
descriptions.get(SERVICE_RESUME_PROGRAM),
schema=RESUME_PROGRAM_SCHEMA)
class Thermostat(ClimateDevice): class Thermostat(ClimateDevice):
"""A thermostat class for Ecobee.""" """A thermostat class for Ecobee."""
@ -249,6 +278,12 @@ class Thermostat(ClimateDevice):
fan_min_on_time) fan_min_on_time)
self.update_without_throttle = True self.update_without_throttle = True
def resume_program(self, resume_all):
"""Resume the thermostat schedule program."""
self.data.ecobee.resume_program(self.thermostat_index,
str(resume_all).lower())
self.update_without_throttle = True
# Home and Sleep mode aren't used in UI yet: # Home and Sleep mode aren't used in UI yet:
# def turn_home_mode_on(self): # def turn_home_mode_on(self):

View File

@ -8,18 +8,27 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, PRECISION_HALVES,
STATE_UNKNOWN, STATE_AUTO, STATE_ON, STATE_OFF,
)
from homeassistant.const import ( from homeassistant.const import (
CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE) CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE)
from homeassistant.util.temperature import convert
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['bluepy_devices==0.2.0'] REQUIREMENTS = ['python-eq3bt==0.1.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_MODE = 'mode' STATE_BOOST = "boost"
ATTR_MODE_READABLE = 'mode_readable' STATE_AWAY = "away"
STATE_MANUAL = "manual"
ATTR_STATE_WINDOW_OPEN = "window_open"
ATTR_STATE_VALVE = "valve"
ATTR_STATE_LOCKED = "is_locked"
ATTR_STATE_LOW_BAT = "low_battery"
DEVICE_SCHEMA = vol.Schema({ DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_MAC): cv.string, vol.Required(CONF_MAC): cv.string,
@ -48,10 +57,23 @@ class EQ3BTSmartThermostat(ClimateDevice):
def __init__(self, _mac, _name): def __init__(self, _mac, _name):
"""Initialize the thermostat.""" """Initialize the thermostat."""
from bluepy_devices.devices import eq3btsmart # we want to avoid name clash with this module..
import eq3bt as eq3
self.modes = {None: STATE_UNKNOWN, # When not yet connected.
eq3.Mode.Unknown: STATE_UNKNOWN,
eq3.Mode.Auto: STATE_AUTO,
# away handled separately, here just for reverse mapping
eq3.Mode.Away: STATE_AWAY,
eq3.Mode.Closed: STATE_OFF,
eq3.Mode.Open: STATE_ON,
eq3.Mode.Manual: STATE_MANUAL,
eq3.Mode.Boost: STATE_BOOST}
self.reverse_modes = {v: k for k, v in self.modes.items()}
self._name = _name self._name = _name
self._thermostat = eq3btsmart.EQ3BTSmartThermostat(_mac) self._thermostat = eq3.Thermostat(_mac)
@property @property
def name(self): def name(self):
@ -63,6 +85,11 @@ class EQ3BTSmartThermostat(ClimateDevice):
"""Return the unit of measurement that is used.""" """Return the unit of measurement that is used."""
return TEMP_CELSIUS return TEMP_CELSIUS
@property
def precision(self):
"""Return eq3bt's precision 0.5."""
return PRECISION_HALVES
@property @property
def current_temperature(self): def current_temperature(self):
"""Can not report temperature, so return target_temperature.""" """Can not report temperature, so return target_temperature."""
@ -81,24 +108,53 @@ class EQ3BTSmartThermostat(ClimateDevice):
self._thermostat.target_temperature = temperature self._thermostat.target_temperature = temperature
@property @property
def device_state_attributes(self): def current_operation(self):
"""Return the device specific state attributes.""" """Current mode."""
return { return self.modes[self._thermostat.mode]
ATTR_MODE: self._thermostat.mode,
ATTR_MODE_READABLE: self._thermostat.mode_readable, @property
} def operation_list(self):
"""List of available operation modes."""
return [x for x in self.modes.values()]
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
self._thermostat.mode = self.reverse_modes[operation_mode]
def turn_away_mode_off(self):
"""Away mode off turns to AUTO mode."""
self.set_operation_mode(STATE_AUTO)
def turn_away_mode_on(self):
"""Set away mode on."""
self.set_operation_mode(STATE_AWAY)
@property
def is_away_mode_on(self):
"""Return if we are away."""
return self.current_operation == STATE_AWAY
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return convert(self._thermostat.min_temp, TEMP_CELSIUS, return self._thermostat.min_temp
self.unit_of_measurement)
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return convert(self._thermostat.max_temp, TEMP_CELSIUS, return self._thermostat.max_temp
self.unit_of_measurement)
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
dev_specific = {
ATTR_STATE_LOCKED: self._thermostat.locked,
ATTR_STATE_LOW_BAT: self._thermostat.low_battery,
ATTR_STATE_VALVE: self._thermostat.valve_state,
ATTR_STATE_WINDOW_OPEN: self._thermostat.window_open,
}
return dev_specific
def update(self): def update(self):
"""Update the data from the thermostat.""" """Update the data from the thermostat."""

View File

@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
} }
devices = {} devices = {}
gateway.platform_callbacks.append(mysensors.pf_callback_factory( gateway.platform_callbacks.append(mysensors.pf_callback_factory(
map_sv_types, devices, add_devices, MySensorsHVAC)) map_sv_types, devices, MySensorsHVAC, add_devices))
class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):

View File

@ -76,7 +76,7 @@ set_operation_mode:
fields: fields:
entity_id: entity_id:
description: Name(s) of entities to change description: Name(s) of entities to change
example: 'climet.nest' example: 'climate.nest'
operation_mode: operation_mode:
description: New value of operation mode description: New value of operation mode
@ -94,3 +94,27 @@ set_swing_mode:
swing_mode: swing_mode:
description: New value of swing mode description: New value of swing mode
example: 1 example: 1
ecobee_set_fan_min_on_time:
description: Set the minimum fan on time
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
fan_min_on_time:
description: New value of fan min on time
example: 5
ecobee_resume_program:
description: Resume the programmed schedule
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
resume_all:
description: Resume all events and return to the scheduled program. This default to false which removes only the top event.
example: true

View File

@ -89,9 +89,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
"""Called when a value has changed on the network.""" """Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \ if self._value.value_id == value.value_id or \
self._value.node == value.node: self._value.node == value.node:
_LOGGER.debug('Value changed for label %s', self._value.label)
self.update_properties() self.update_properties()
self.schedule_update_ha_state() self.schedule_update_ha_state()
_LOGGER.debug("Value changed on network %s", value)
def update_properties(self): def update_properties(self):
"""Callback on data change for the registered node/value pair.""" """Callback on data change for the registered node/value pair."""

View File

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover/ https://home-assistant.io/components/cover/
""" """
import os import os
from datetime import timedelta
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -23,7 +24,7 @@ from homeassistant.const import (
DOMAIN = 'cover' DOMAIN = 'cover'
SCAN_INTERVAL = 15 SCAN_INTERVAL = timedelta(seconds=15)
GROUP_NAME_ALL_COVERS = 'all covers' GROUP_NAME_ALL_COVERS = 'all covers'
ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers') ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers')

View File

@ -35,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
}) })
devices = {} devices = {}
gateway.platform_callbacks.append(mysensors.pf_callback_factory( gateway.platform_callbacks.append(mysensors.pf_callback_factory(
map_sv_types, devices, add_devices, MySensorsCover)) map_sv_types, devices, MySensorsCover, add_devices))
class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice): class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice):

View File

@ -78,9 +78,9 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
"""Called when a value has changed on the network.""" """Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \ if self._value.value_id == value.value_id or \
self._value.node == value.node: self._value.node == value.node:
_LOGGER.debug('Value changed for label %s', self._value.label)
self.update_properties() self.update_properties()
self.update_ha_state() self.schedule_update_ha_state()
_LOGGER.debug("Value changed on network %s", value)
def update_properties(self): def update_properties(self):
"""Callback on data change for the registered node/value pair.""" """Callback on data change for the registered node/value pair."""
@ -170,9 +170,9 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
def value_changed(self, value): def value_changed(self, value):
"""Called when a value has changed on the network.""" """Called when a value has changed on the network."""
if self._value.value_id == value.value_id: if self._value.value_id == value.value_id:
_LOGGER.debug('Value changed for label %s', self._value.label)
self._state = value.data self._state = value.data
self.update_ha_state() self.schedule_update_ha_state()
_LOGGER.debug("Value changed on network %s", value)
@property @property
def is_closed(self): def is_closed(self):

View File

@ -23,12 +23,14 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
'cover', 'cover',
'device_tracker', 'device_tracker',
'fan', 'fan',
'image_processing',
'light', 'light',
'lock', 'lock',
'media_player', 'media_player',
'notify', 'notify',
'sensor', 'sensor',
'switch', 'switch',
'tts',
] ]

View File

@ -8,7 +8,7 @@ import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
import os import os
from typing import Any, Sequence, Callable from typing import Any, List, Sequence, Callable
import aiohttp import aiohttp
import async_timeout import async_timeout
@ -24,6 +24,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util as util import homeassistant.util as util
@ -50,10 +51,10 @@ CONF_TRACK_NEW = 'track_new_devices'
DEFAULT_TRACK_NEW = True DEFAULT_TRACK_NEW = True
CONF_CONSIDER_HOME = 'consider_home' CONF_CONSIDER_HOME = 'consider_home'
DEFAULT_CONSIDER_HOME = 180 DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
CONF_SCAN_INTERVAL = 'interval_seconds' CONF_SCAN_INTERVAL = 'interval_seconds'
DEFAULT_SCAN_INTERVAL = 12 DEFAULT_SCAN_INTERVAL = timedelta(seconds=12)
CONF_AWAY_HIDE = 'hide_if_away' CONF_AWAY_HIDE = 'hide_if_away'
DEFAULT_AWAY_HIDE = False DEFAULT_AWAY_HIDE = False
@ -69,12 +70,16 @@ ATTR_LOCATION_NAME = 'location_name'
ATTR_GPS = 'gps' ATTR_GPS = 'gps'
ATTR_BATTERY = 'battery' ATTR_BATTERY = 'battery'
ATTR_ATTRIBUTES = 'attributes' ATTR_ATTRIBUTES = 'attributes'
ATTR_SOURCE_TYPE = 'source_type'
SOURCE_TYPE_GPS = 'gps'
SOURCE_TYPE_ROUTER = 'router'
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_INTERVAL): cv.positive_int, # seconds vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
vol.Optional(CONF_CONSIDER_HOME, vol.Optional(CONF_CONSIDER_HOME,
default=timedelta(seconds=DEFAULT_CONSIDER_HOME)): vol.All( default=DEFAULT_CONSIDER_HOME): vol.All(
cv.time_period, cv.positive_timedelta) cv.time_period, cv.positive_timedelta)
}) })
@ -121,8 +126,7 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
return False return False
else: else:
conf = conf[0] if len(conf) > 0 else {} conf = conf[0] if len(conf) > 0 else {}
consider_home = conf.get(CONF_CONSIDER_HOME, consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
timedelta(seconds=DEFAULT_CONSIDER_HOME))
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
devices = yield from async_load_config(yaml_path, hass, consider_home) devices = yield from async_load_config(yaml_path, hass, consider_home)
@ -142,23 +146,34 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
if platform is None: if platform is None:
return return
_LOGGER.info("Setting up %s.%s", DOMAIN, p_type)
try: try:
if hasattr(platform, 'get_scanner'): scanner = None
setup = None
if hasattr(platform, 'async_get_scanner'):
scanner = yield from platform.async_get_scanner(
hass, {DOMAIN: p_config})
elif hasattr(platform, 'get_scanner'):
scanner = yield from hass.loop.run_in_executor( scanner = yield from hass.loop.run_in_executor(
None, platform.get_scanner, hass, {DOMAIN: p_config}) None, platform.get_scanner, hass, {DOMAIN: p_config})
elif hasattr(platform, 'async_setup_scanner'):
setup = yield from platform.async_setup_scanner(
hass, p_config, tracker.async_see)
elif hasattr(platform, 'setup_scanner'):
setup = yield from hass.loop.run_in_executor(
None, platform.setup_scanner, hass, p_config, tracker.see)
else:
raise HomeAssistantError("Invalid device_tracker platform.")
if scanner is None: if scanner:
_LOGGER.error('Error setting up platform %s', p_type)
return
yield from async_setup_scanner_platform( yield from async_setup_scanner_platform(
hass, p_config, scanner, tracker.async_see) hass, p_config, scanner, tracker.async_see)
return return
ret = yield from hass.loop.run_in_executor( if not setup:
None, platform.setup_scanner, hass, p_config, tracker.see)
if not ret:
_LOGGER.error('Error setting up platform %s', p_type) _LOGGER.error('Error setting up platform %s', p_type)
return
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error setting up platform %s', p_type) _LOGGER.exception('Error setting up platform %s', p_type)
@ -223,17 +238,19 @@ class DeviceTracker(object):
def see(self, mac: str=None, dev_id: str=None, host_name: str=None, def see(self, mac: str=None, dev_id: str=None, host_name: str=None,
location_name: str=None, gps: GPSType=None, gps_accuracy=None, location_name: str=None, gps: GPSType=None, gps_accuracy=None,
battery: str=None, attributes: dict=None): battery: str=None, attributes: dict=None,
source_type: str=SOURCE_TYPE_GPS):
"""Notify the device tracker that you see a device.""" """Notify the device tracker that you see a device."""
self.hass.add_job( self.hass.add_job(
self.async_see(mac, dev_id, host_name, location_name, gps, self.async_see(mac, dev_id, host_name, location_name, gps,
gps_accuracy, battery, attributes) gps_accuracy, battery, attributes, source_type)
) )
@asyncio.coroutine @asyncio.coroutine
def async_see(self, mac: str=None, dev_id: str=None, host_name: str=None, def async_see(self, mac: str=None, dev_id: str=None, host_name: str=None,
location_name: str=None, gps: GPSType=None, location_name: str=None, gps: GPSType=None,
gps_accuracy=None, battery: str=None, attributes: dict=None): gps_accuracy=None, battery: str=None, attributes: dict=None,
source_type: str=SOURCE_TYPE_GPS):
"""Notify the device tracker that you see a device. """Notify the device tracker that you see a device.
This method is a coroutine. This method is a coroutine.
@ -251,7 +268,8 @@ class DeviceTracker(object):
if device: if device:
yield from device.async_seen(host_name, location_name, gps, yield from device.async_seen(host_name, location_name, gps,
gps_accuracy, battery, attributes) gps_accuracy, battery, attributes,
source_type)
if device.track: if device.track:
yield from device.async_update_ha_state() yield from device.async_update_ha_state()
return return
@ -266,7 +284,8 @@ class DeviceTracker(object):
self.mac_to_dev[mac] = device self.mac_to_dev[mac] = device
yield from device.async_seen(host_name, location_name, gps, yield from device.async_seen(host_name, location_name, gps,
gps_accuracy, battery, attributes) gps_accuracy, battery, attributes,
source_type)
if device.track: if device.track:
yield from device.async_update_ha_state() yield from device.async_update_ha_state()
@ -370,6 +389,9 @@ class Device(Entity):
self.away_hide = hide_if_away self.away_hide = hide_if_away
self.vendor = vendor self.vendor = vendor
self.source_type = None
self._attributes = {} self._attributes = {}
@property @property
@ -390,7 +412,9 @@ class Device(Entity):
@property @property
def state_attributes(self): def state_attributes(self):
"""Return the device state attributes.""" """Return the device state attributes."""
attr = {} attr = {
ATTR_SOURCE_TYPE: self.source_type
}
if self.gps: if self.gps:
attr[ATTR_LATITUDE] = self.gps[0] attr[ATTR_LATITUDE] = self.gps[0]
@ -415,12 +439,13 @@ class Device(Entity):
@asyncio.coroutine @asyncio.coroutine
def async_seen(self, host_name: str=None, location_name: str=None, def async_seen(self, host_name: str=None, location_name: str=None,
gps: GPSType=None, gps_accuracy=0, battery: str=None, gps: GPSType=None, gps_accuracy=0, battery: str=None,
attributes: dict=None): attributes: dict=None, source_type: str=SOURCE_TYPE_GPS):
"""Mark the device as seen.""" """Mark the device as seen."""
self.source_type = source_type
self.last_seen = dt_util.utcnow() self.last_seen = dt_util.utcnow()
self.host_name = host_name self.host_name = host_name
self.location_name = location_name self.location_name = location_name
self.gps_accuracy = gps_accuracy or 0
if battery: if battery:
self.battery = battery self.battery = battery
if attributes: if attributes:
@ -431,7 +456,10 @@ class Device(Entity):
if gps is not None: if gps is not None:
try: try:
self.gps = float(gps[0]), float(gps[1]) self.gps = float(gps[0]), float(gps[1])
self.gps_accuracy = gps_accuracy or 0
except (ValueError, TypeError, IndexError): except (ValueError, TypeError, IndexError):
self.gps = None
self.gps_accuracy = 0
_LOGGER.warning('Could not parse gps value for %s: %s', _LOGGER.warning('Could not parse gps value for %s: %s',
self.dev_id, gps) self.dev_id, gps)
@ -456,7 +484,7 @@ class Device(Entity):
return return
elif self.location_name: elif self.location_name:
self._state = self.location_name self._state = self.location_name
elif self.gps is not None: elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS:
zone_state = zone.async_active_zone( zone_state = zone.async_active_zone(
self.hass, self.gps[0], self.gps[1], self.gps_accuracy) self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
if zone_state is None: if zone_state is None:
@ -465,9 +493,9 @@ class Device(Entity):
self._state = STATE_HOME self._state = STATE_HOME
else: else:
self._state = zone_state.name self._state = zone_state.name
elif self.stale(): elif self.stale():
self._state = STATE_NOT_HOME self._state = STATE_NOT_HOME
self.gps = None
self.last_update_home = False self.last_update_home = False
else: else:
self._state = STATE_HOME self._state = STATE_HOME
@ -485,13 +513,18 @@ class Device(Entity):
if not self.mac: if not self.mac:
return None return None
if '_' in self.mac:
_, mac = self.mac.split('_', 1)
else:
mac = self.mac
# prevent lookup of invalid macs # prevent lookup of invalid macs
if not len(self.mac.split(':')) == 6: if not len(mac.split(':')) == 6:
return 'unknown' return 'unknown'
# we only need the first 3 bytes of the mac for a lookup # we only need the first 3 bytes of the mac for a lookup
# this improves somewhat on privacy # this improves somewhat on privacy
oui_bytes = self.mac.split(':')[0:3] oui_bytes = mac.split(':')[0:3]
# bytes like 00 get truncates to 0, API needs full bytes # bytes like 00 get truncates to 0, API needs full bytes
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes]) oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
url = 'http://api.macvendors.com/' + oui url = 'http://api.macvendors.com/' + oui
@ -521,6 +554,34 @@ class Device(Entity):
yield from resp.release() yield from resp.release()
class DeviceScanner(object):
"""Device scanner object."""
hass = None # type: HomeAssistantType
def scan_devices(self) -> List[str]:
"""Scan for devices."""
raise NotImplementedError()
def async_scan_devices(self) -> Any:
"""Scan for devices.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(None, self.scan_devices)
def get_device_name(self, mac: str) -> str:
"""Get device name from mac."""
raise NotImplementedError()
def async_get_device_name(self, mac: str) -> Any:
"""Get device name from mac.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(None, self.get_device_name, mac)
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
"""Load devices from YAML configuration file.""" """Load devices from YAML configuration file."""
return run_coroutine_threadsafe( return run_coroutine_threadsafe(
@ -577,26 +638,39 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
This method is a coroutine. This method is a coroutine.
""" """
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
scanner.hass = hass
# Initial scan of each mac we also tell about host name for config # Initial scan of each mac we also tell about host name for config
seen = set() # type: Any seen = set() # type: Any
def device_tracker_scan(now: dt_util.dt.datetime): @asyncio.coroutine
def async_device_tracker_scan(now: dt_util.dt.datetime):
"""Called when interval matches.""" """Called when interval matches."""
found_devices = scanner.scan_devices() found_devices = yield from scanner.async_scan_devices()
for mac in found_devices: for mac in found_devices:
if mac in seen: if mac in seen:
host_name = None host_name = None
else: else:
host_name = scanner.get_device_name(mac) host_name = yield from scanner.async_get_device_name(mac)
seen.add(mac) seen.add(mac)
hass.add_job(async_see_device(mac=mac, host_name=host_name))
async_track_utc_time_change( kwargs = {
hass, device_tracker_scan, second=range(0, 60, interval)) 'mac': mac,
'host_name': host_name,
'source_type': SOURCE_TYPE_ROUTER
}
hass.async_add_job(device_tracker_scan, None) zone_home = hass.states.get(zone.ENTITY_ID_HOME)
if zone_home:
kwargs['gps'] = [zone_home.attributes[ATTR_LATITUDE],
zone_home.attributes[ATTR_LONGITUDE]]
kwargs['gps_accuracy'] = 0
hass.async_add_job(async_see_device(**kwargs))
async_track_time_interval(hass, async_device_tracker_scan, interval)
hass.async_add_job(async_device_tracker_scan, None)
def update_config(path: str, dev_id: str, device: Device): def update_config(path: str, dev_id: str, device: Device):

View File

@ -14,7 +14,8 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA) from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -46,7 +47,7 @@ def get_scanner(hass, config):
Device = namedtuple("Device", ["mac", "ip", "last_update"]) Device = namedtuple("Device", ["mac", "ip", "last_update"])
class ActiontecDeviceScanner(object): class ActiontecDeviceScanner(DeviceScanner):
"""This class queries a an actiontec router for connected devices.""" """This class queries a an actiontec router for connected devices."""
def __init__(self, config): def __init__(self, config):

View File

@ -12,7 +12,8 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -42,7 +43,7 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
class ArubaDeviceScanner(object): class ArubaDeviceScanner(DeviceScanner):
"""This class queries a Aruba Access Point for connected devices.""" """This class queries a Aruba Access Point for connected devices."""
def __init__(self, config): def __init__(self, config):

View File

@ -14,7 +14,8 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -97,7 +98,7 @@ def get_scanner(hass, config):
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram') AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram')
class AsusWrtDeviceScanner(object): class AsusWrtDeviceScanner(DeviceScanner):
"""This class queries a router running ASUSWRT firmware.""" """This class queries a router running ASUSWRT firmware."""
# Eighth attribute needed for mode (AP mode vs router mode) # Eighth attribute needed for mode (AP mode vs router mode)

View File

@ -11,8 +11,8 @@ import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.components.device_tracker import (PLATFORM_SCHEMA, from homeassistant.components.device_tracker import (
ATTR_ATTRIBUTES) PLATFORM_SCHEMA, ATTR_ATTRIBUTES)
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change

View File

@ -9,7 +9,7 @@ import logging
from datetime import timedelta from datetime import timedelta
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.device_tracker import DOMAIN, DeviceScanner
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ['pybbox==0.0.5-alpha'] REQUIREMENTS = ['pybbox==0.0.5-alpha']
@ -29,7 +29,7 @@ def get_scanner(hass, config):
Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update']) Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update'])
class BboxDeviceScanner(object): class BboxDeviceScanner(DeviceScanner):
"""This class scans for devices connected to the bbox.""" """This class scans for devices connected to the bbox."""
def __init__(self, config): def __init__(self, config):

View File

@ -5,13 +5,8 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
YAML_DEVICES, YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
CONF_TRACK_NEW, PLATFORM_SCHEMA, load_config, DEFAULT_TRACK_NEW
CONF_SCAN_INTERVAL,
DEFAULT_SCAN_INTERVAL,
PLATFORM_SCHEMA,
load_config,
DEFAULT_TRACK_NEW
) )
import homeassistant.util as util import homeassistant.util as util
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -24,9 +19,11 @@ REQUIREMENTS = ['gattlib==0.20150805']
BLE_PREFIX = 'BLE_' BLE_PREFIX = 'BLE_'
MIN_SEEN_NEW = 5 MIN_SEEN_NEW = 5
CONF_SCAN_DURATION = "scan_duration" CONF_SCAN_DURATION = "scan_duration"
CONF_BLUETOOTH_DEVICE = "device_id"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_DURATION, default=10): cv.positive_int vol.Optional(CONF_SCAN_DURATION, default=10): cv.positive_int,
vol.Optional(CONF_BLUETOOTH_DEVICE, default="hci0"): cv.string
}) })
@ -60,7 +57,7 @@ def setup_scanner(hass, config, see):
"""Discover Bluetooth LE devices.""" """Discover Bluetooth LE devices."""
_LOGGER.debug("Discovering Bluetooth LE devices") _LOGGER.debug("Discovering Bluetooth LE devices")
try: try:
service = DiscoveryService() service = DiscoveryService(ble_dev_id)
devices = service.discover(duration) devices = service.discover(duration)
_LOGGER.debug("Bluetooth LE devices discovered = %s", devices) _LOGGER.debug("Bluetooth LE devices discovered = %s", devices)
except RuntimeError as error: except RuntimeError as error:
@ -70,6 +67,7 @@ def setup_scanner(hass, config, see):
yaml_path = hass.config.path(YAML_DEVICES) yaml_path = hass.config.path(YAML_DEVICES)
duration = config.get(CONF_SCAN_DURATION) duration = config.get(CONF_SCAN_DURATION)
ble_dev_id = config.get(CONF_BLUETOOTH_DEVICE)
devs_to_track = [] devs_to_track = []
devs_donot_track = [] devs_donot_track = []

View File

@ -16,7 +16,8 @@ import requests
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -40,7 +41,7 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
class BTHomeHub5DeviceScanner(object): class BTHomeHub5DeviceScanner(DeviceScanner):
"""This class queries a BT Home Hub 5.""" """This class queries a BT Home Hub 5."""
def __init__(self, config): def __init__(self, config):

View File

@ -10,7 +10,8 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \
CONF_PORT CONF_PORT
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -39,7 +40,7 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
class CiscoDeviceScanner(object): class CiscoDeviceScanner(DeviceScanner):
"""This class queries a wireless router running Cisco IOS firmware.""" """This class queries a wireless router running Cisco IOS firmware."""
def __init__(self, config): def __init__(self, config):

View File

@ -13,7 +13,8 @@ import requests
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -41,7 +42,7 @@ def get_scanner(hass, config):
return None return None
class DdWrtDeviceScanner(object): class DdWrtDeviceScanner(DeviceScanner):
"""This class queries a wireless router running DD-WRT firmware.""" """This class queries a wireless router running DD-WRT firmware."""
def __init__(self, config): def __init__(self, config):

View File

@ -10,7 +10,8 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -38,7 +39,7 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
class FritzBoxScanner(object): class FritzBoxScanner(DeviceScanner):
"""This class queries a FRITZ!Box router.""" """This class queries a FRITZ!Box router."""
def __init__(self, config): def __init__(self, config):

View File

@ -12,7 +12,7 @@ import voluptuous as vol
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT) PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT, DeviceScanner)
from homeassistant.components.zone import active_zone from homeassistant.components.zone import active_zone
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -131,7 +131,7 @@ def setup_scanner(hass, config: dict, see):
return True return True
class Icloud(object): class Icloud(DeviceScanner):
"""Represent an icloud account in Home Assistant.""" """Represent an icloud account in Home Assistant."""
def __init__(self, hass, username, password, name, see): def __init__(self, hass, username, password, name, see):

View File

@ -8,9 +8,8 @@ import asyncio
from functools import partial from functools import partial
import logging import logging
from homeassistant.const import (ATTR_LATITUDE, ATTR_LONGITUDE, from homeassistant.const import (
STATE_NOT_HOME, ATTR_LATITUDE, ATTR_LONGITUDE, STATE_NOT_HOME, HTTP_UNPROCESSABLE_ENTITY)
HTTP_UNPROCESSABLE_ENTITY)
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
# pylint: disable=unused-import # pylint: disable=unused-import
from homeassistant.components.device_tracker import ( # NOQA from homeassistant.components.device_tracker import ( # NOQA
@ -64,18 +63,18 @@ class LocativeView(HomeAssistantView):
return ('Device id not specified.', return ('Device id not specified.',
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY)
if 'id' not in data:
_LOGGER.error('Location id not specified.')
return ('Location id not specified.',
HTTP_UNPROCESSABLE_ENTITY)
if 'trigger' not in data: if 'trigger' not in data:
_LOGGER.error('Trigger is not specified.') _LOGGER.error('Trigger is not specified.')
return ('Trigger is not specified.', return ('Trigger is not specified.',
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY)
if 'id' not in data and data['trigger'] != 'test':
_LOGGER.error('Location id not specified.')
return ('Location id not specified.',
HTTP_UNPROCESSABLE_ENTITY)
device = data['device'].replace('-', '') device = data['device'].replace('-', '')
location_name = data['id'].lower() location_name = data.get('id', data['trigger']).lower()
direction = data['trigger'] direction = data['trigger']
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])

View File

@ -14,7 +14,8 @@ import requests
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -37,7 +38,7 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
class LuciDeviceScanner(object): class LuciDeviceScanner(DeviceScanner):
"""This class queries a wireless router running OpenWrt firmware. """This class queries a wireless router running OpenWrt firmware.
Adapted from Tomato scanner. Adapted from Tomato scanner.

View File

@ -11,7 +11,8 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -47,7 +48,7 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
class NetgearDeviceScanner(object): class NetgearDeviceScanner(DeviceScanner):
"""Queries a Netgear wireless router using the SOAP-API.""" """Queries a Netgear wireless router using the SOAP-API."""
def __init__(self, host, username, password, port): def __init__(self, host, username, password, port):

View File

@ -14,7 +14,8 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOSTS from homeassistant.const import CONF_HOSTS
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -63,7 +64,7 @@ def _arp(ip_address):
return None return None
class NmapDeviceScanner(object): class NmapDeviceScanner(DeviceScanner):
"""This class scans for devices using nmap.""" """This class scans for devices using nmap."""
exclude = [] exclude = []

View File

@ -0,0 +1,92 @@
"""
Tracks devices by sending a ICMP ping.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.ping/
device_tracker:
- platform: ping
count: 2
hosts:
host_one: pc.local
host_two: 192.168.2.25
"""
import logging
import subprocess
import sys
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, DEFAULT_SCAN_INTERVAL)
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant import util
from homeassistant import const
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = []
_LOGGER = logging.getLogger(__name__)
CONF_PING_COUNT = 'count'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(const.CONF_HOSTS): {cv.string: cv.string},
vol.Optional(CONF_PING_COUNT, default=1): cv.positive_int,
})
class Host:
"""Host object with ping detection."""
def __init__(self, ip_address, dev_id, hass, config):
"""Initialize the Host pinger."""
self.hass = hass
self.ip_address = ip_address
self.dev_id = dev_id
self._count = config[CONF_PING_COUNT]
if sys.platform == "win32":
self._ping_cmd = ['ping', '-n 1', '-w 1000', self.ip_address]
else:
self._ping_cmd = ['ping', '-n', '-q', '-c1', '-W1',
self.ip_address]
def ping(self):
"""Send ICMP ping and return True if success."""
pinger = subprocess.Popen(self._ping_cmd, stdout=subprocess.PIPE)
try:
pinger.communicate()
return pinger.returncode == 0
except subprocess.CalledProcessError:
return False
def update(self, see):
"""Update device state by sending one or more ping messages."""
failed = 0
while failed < self._count: # check more times if host in unreachable
if self.ping():
see(dev_id=self.dev_id)
return True
failed += 1
_LOGGER.debug("ping KO on ip=%s failed=%d", self.ip_address, failed)
def setup_scanner(hass, config, see):
"""Setup the Host objects and return the update function."""
hosts = [Host(ip, dev_id, hass, config) for (dev_id, ip) in
config[const.CONF_HOSTS].items()]
interval = timedelta(seconds=len(hosts) * config[CONF_PING_COUNT]) + \
DEFAULT_SCAN_INTERVAL
_LOGGER.info("Started ping tracker with interval=%s on hosts: %s",
interval, ",".join([host.ip_address for host in hosts]))
def update(now):
"""Update all the hosts on every interval time."""
for host in hosts:
host.update(see)
track_point_in_utc_time(hass, update, now + interval)
return True
return update(util.dt.utcnow())

View File

@ -12,7 +12,8 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -46,7 +47,7 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
class SnmpScanner(object): class SnmpScanner(DeviceScanner):
"""Queries any SNMP capable Access Point for connected devices.""" """Queries any SNMP capable Access Point for connected devices."""
def __init__(self, config): def __init__(self, config):

View File

@ -12,7 +12,8 @@ import requests
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -35,7 +36,7 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
class SwisscomDeviceScanner(object): class SwisscomDeviceScanner(DeviceScanner):
"""This class queries a router running Swisscom Internet-Box firmware.""" """This class queries a router running Swisscom Internet-Box firmware."""
def __init__(self, config): def __init__(self, config):

View File

@ -13,7 +13,8 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -46,7 +47,7 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
class ThomsonDeviceScanner(object): class ThomsonDeviceScanner(DeviceScanner):
"""This class queries a router running THOMSON firmware.""" """This class queries a router running THOMSON firmware."""
def __init__(self, config): def __init__(self, config):

View File

@ -14,7 +14,8 @@ import requests
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -38,7 +39,7 @@ def get_scanner(hass, config):
return TomatoDeviceScanner(config[DOMAIN]) return TomatoDeviceScanner(config[DOMAIN])
class TomatoDeviceScanner(object): class TomatoDeviceScanner(DeviceScanner):
"""This class queries a wireless router running Tomato firmware.""" """This class queries a wireless router running Tomato firmware."""
def __init__(self, config): def __init__(self, config):

View File

@ -15,7 +15,8 @@ import requests
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -42,7 +43,7 @@ def get_scanner(hass, config):
return None return None
class TplinkDeviceScanner(object): class TplinkDeviceScanner(DeviceScanner):
"""This class queries a wireless router running TP-Link firmware.""" """This class queries a wireless router running TP-Link firmware."""
def __init__(self, config): def __init__(self, config):

View File

@ -0,0 +1,79 @@
"""
Support for the TrackR platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.trackr/
"""
import logging
import voluptuous as vol
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_utc_time_change
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pytrackr==0.0.5']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
def setup_scanner(hass, config: dict, see):
"""Validate the configuration and return a TrackR scanner."""
TrackRDeviceScanner(hass, config, see)
return True
class TrackRDeviceScanner(object):
"""A class representing a TrackR device."""
def __init__(self, hass, config: dict, see) -> None:
"""Initialize the TrackR device scanner."""
from pytrackr.api import trackrApiInterface
self.hass = hass
self.api = trackrApiInterface(config.get(CONF_USERNAME),
config.get(CONF_PASSWORD))
self.see = see
self.devices = self.api.get_trackrs()
self._update_info()
track_utc_time_change(self.hass, self._update_info,
second=range(0, 60, 30))
def _update_info(self, now=None) -> None:
"""Update the device info."""
_LOGGER.debug('Updating devices %s', now)
# Update self.devices to collect new devices added
# to the users account.
self.devices = self.api.get_trackrs()
for trackr in self.devices:
trackr.update_state()
trackr_id = trackr.tracker_id()
trackr_device_id = trackr.id()
lost = trackr.lost()
dev_id = trackr.name().replace(" ", "_")
if dev_id is None:
dev_id = trackr_id
location = trackr.last_known_location()
lat = location['latitude']
lon = location['longitude']
attrs = {
'last_updated': trackr.last_updated(),
'last_seen': trackr.last_time_seen(),
'trackr_id': trackr_id,
'id': trackr_device_id,
'lost': lost,
'battery_level': trackr.battery_level()
}
self.see(
dev_id=dev_id, gps=(lat, lon), attributes=attrs
)

View File

@ -14,7 +14,8 @@ import requests
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -37,7 +38,7 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
class UbusDeviceScanner(object): class UbusDeviceScanner(DeviceScanner):
""" """
This class queries a wireless router running OpenWrt firmware. This class queries a wireless router running OpenWrt firmware.

View File

@ -10,7 +10,8 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.loader as loader import homeassistant.loader as loader
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
# Unifi package doesn't list urllib3 as a requirement # Unifi package doesn't list urllib3 as a requirement
@ -59,7 +60,7 @@ def get_scanner(hass, config):
return UnifiScanner(ctrl) return UnifiScanner(ctrl)
class UnifiScanner(object): class UnifiScanner(DeviceScanner):
"""Provide device_tracker support from Unifi WAP client data.""" """Provide device_tracker support from Unifi WAP client data."""
def __init__(self, controller): def __init__(self, controller):

View File

@ -0,0 +1,164 @@
"""
Support for UPC ConnectBox router.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.upc_connect/
"""
import asyncio
import logging
import xml.etree.ElementTree as ET
import aiohttp
import async_timeout
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_create_clientsession
_LOGGER = logging.getLogger(__name__)
DEFAULT_IP = '192.168.0.1'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string,
})
CMD_LOGIN = 15
CMD_DEVICES = 123
@asyncio.coroutine
def async_get_scanner(hass, config):
"""Return the UPC device scanner."""
scanner = UPCDeviceScanner(hass, config[DOMAIN])
success_init = yield from scanner.async_login()
return scanner if success_init else None
class UPCDeviceScanner(DeviceScanner):
"""This class queries a router running UPC ConnectBox firmware."""
def __init__(self, hass, config):
"""Initialize the scanner."""
self.hass = hass
self.host = config[CONF_HOST]
self.password = config[CONF_PASSWORD]
self.data = {}
self.token = None
self.headers = {
'X-Requested-With': 'XMLHttpRequest',
'Referer': "http://{}/index.html".format(self.host),
'User-Agent': ("Mozilla/5.0 (Windows NT 10.0; WOW64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/47.0.2526.106 Safari/537.36")
}
self.websession = async_create_clientsession(
hass, cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop))
@asyncio.coroutine
def async_scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
if self.token is None:
reconnect = yield from self.async_login()
if not reconnect:
_LOGGER.error("Not connected to %s", self.host)
return []
raw = yield from self._async_ws_function(CMD_DEVICES)
xml_root = ET.fromstring(raw)
return [mac.text for mac in xml_root.iter('MACAddr')]
@asyncio.coroutine
def async_get_device_name(self, device):
"""The firmware doesn't save the name of the wireless device."""
return None
@asyncio.coroutine
def async_login(self):
"""Login into firmware and get first token."""
response = None
try:
# get first token
with async_timeout.timeout(10, loop=self.hass.loop):
response = yield from self.websession.get(
"http://{}/common_page/login.html".format(self.host)
)
self.token = self._async_get_token()
# login
data = yield from self._async_ws_function(CMD_LOGIN, {
'Username': 'NULL',
'Password': self.password,
})
# successfull?
if data.find("successful") != -1:
return True
return False
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.error("Can not load login page from %s", self.host)
return False
finally:
if response is not None:
yield from response.release()
@asyncio.coroutine
def _async_ws_function(self, function, additional_form=None):
"""Execute a command on UPC firmware webservice."""
form_data = {
'token': self.token,
'fun': function
}
if additional_form:
form_data.update(additional_form)
response = None
try:
with async_timeout.timeout(10, loop=self.hass.loop):
response = yield from self.websession.post(
"http://{}/xml/getter.xml".format(self.host),
data=form_data,
headers=self.headers
)
# error on UPC webservice
if response.status != 200:
_LOGGER.warning(
"Error %d on %s.", response.status, function)
self.token = None
return
# load data, store token for next request
raw = yield from response.text()
self.token = self._async_get_token()
return raw
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.error("Error on %s", function)
self.token = None
finally:
if response is not None:
yield from response.release()
def _async_get_token(self):
"""Extract token from cookies."""
cookie_manager = self.websession.cookie_jar.filter_cookies(
"http://{}".format(self.host))
return cookie_manager.get('sessionToken')

View File

@ -14,12 +14,9 @@ from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.const import ( from homeassistant.const import (
CONF_PASSWORD, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME)
CONF_SCAN_INTERVAL,
CONF_USERNAME)
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, PLATFORM_SCHEMA)
PLATFORM_SCHEMA)
MIN_TIME_BETWEEN_SCANS = timedelta(minutes=1) MIN_TIME_BETWEEN_SCANS = timedelta(minutes=1)

View File

@ -0,0 +1,145 @@
"""
Support for Xiaomi Mi routers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.xiaomi/
"""
import logging
import threading
from datetime import timedelta
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME, default='admin'): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
def get_scanner(hass, config):
"""Validate the configuration and return a Xiaomi Device Scanner."""
scanner = XioamiDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class XioamiDeviceScanner(DeviceScanner):
"""This class queries a Xiaomi Mi router.
Adapted from Luci scanner.
"""
def __init__(self, config):
"""Initialize the scanner."""
host = config[CONF_HOST]
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = {}
self.token = _get_token(host, username, password)
self.host = host
self.mac2name = None
self.success_init = self.token is not None
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return self.last_results
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
with self.lock:
if self.mac2name is None:
url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/devicelist"
url = url.format(self.host, self.token)
result = _get_device_list(url)
if result:
hosts = [x for x in result
if 'mac' in x and 'name' in x]
mac2name_list = [
(x['mac'].upper(), x['name']) for x in hosts]
self.mac2name = dict(mac2name_list)
else:
# Error, handled in the _req_json_rpc
return
return self.mac2name.get(device.upper(), None)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the informations from the router are up to date.
Returns true if scanning successful.
"""
if not self.success_init:
return False
with self.lock:
_LOGGER.info('Refreshing device list')
url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/devicelist"
url = url.format(self.host, self.token)
result = _get_device_list(url)
if result:
self.last_results = []
for device_entry in result:
# Check if the device is marked as connected
if int(device_entry['online']) == 1:
self.last_results.append(device_entry['mac'])
return True
return False
def _get_device_list(url, **kwargs):
try:
res = requests.get(url, timeout=5, **kwargs)
except requests.exceptions.Timeout:
_LOGGER.exception('Connection to the router timed out')
return
return _extract_result(res, 'list')
def _get_token(host, username, password):
"""Get authentication token for the given host+username+password."""
url = 'http://{}/cgi-bin/luci/api/xqsystem/login'.format(host)
data = {'username': username, 'password': password}
try:
res = requests.post(url, data=data, timeout=5)
except requests.exceptions.Timeout:
_LOGGER.exception('Connection to the router timed out')
return
return _extract_result(res, 'token')
def _extract_result(res, key_name):
if res.status_code == 200:
try:
result = res.json()
except ValueError:
# If json decoder could not parse the response
_LOGGER.exception('Failed to parse response from mi router')
return
try:
return result[key_name]
except KeyError:
_LOGGER.exception('No %s in response from mi router. %s',
key_name, result)
return
else:
_LOGGER.error('Invalid response from mi router: %s', res)

View File

@ -38,6 +38,7 @@ SERVICE_HANDLERS = {
'directv': ('media_player', 'directv'), 'directv': ('media_player', 'directv'),
'denonavr': ('media_player', 'denonavr'), 'denonavr': ('media_player', 'denonavr'),
'samsung_tv': ('media_player', 'samsungtv'), 'samsung_tv': ('media_player', 'samsungtv'),
'yeelight': ('light', 'yeelight'),
} }
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({

View File

@ -7,7 +7,8 @@ from aiohttp import web
from homeassistant import core from homeassistant import core
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET, ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET,
STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, STATE_ON, STATE_OFF,
HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
) )
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
@ -90,7 +91,7 @@ class HueOneLightStateView(HomeAssistantView):
self.config = config self.config = config
@core.callback @core.callback
def get(self, request, username, entity_id=None): def get(self, request, username, entity_id):
"""Process a request to get the state of an individual light.""" """Process a request to get the state of an individual light."""
hass = request.app['hass'] hass = request.app['hass']
entity_id = self.config.number_to_entity_id(entity_id) entity_id = self.config.number_to_entity_id(entity_id)
@ -161,6 +162,9 @@ class HueOneLightChangeView(HomeAssistantView):
# Choose general HA domain # Choose general HA domain
domain = core.DOMAIN domain = core.DOMAIN
# Entity needs separate call to turn on
turn_on_needed = False
# Convert the resulting "on" status into the service we need to call # Convert the resulting "on" status into the service we need to call
service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
@ -189,11 +193,20 @@ class HueOneLightChangeView(HomeAssistantView):
ATTR_SUPPORTED_MEDIA_COMMANDS, 0) ATTR_SUPPORTED_MEDIA_COMMANDS, 0)
if media_commands & SUPPORT_VOLUME_SET == SUPPORT_VOLUME_SET: if media_commands & SUPPORT_VOLUME_SET == SUPPORT_VOLUME_SET:
if brightness is not None: if brightness is not None:
turn_on_needed = True
domain = entity.domain domain = entity.domain
service = SERVICE_VOLUME_SET service = SERVICE_VOLUME_SET
# Convert 0-100 to 0.0-1.0 # Convert 0-100 to 0.0-1.0
data[ATTR_MEDIA_VOLUME_LEVEL] = brightness / 100.0 data[ATTR_MEDIA_VOLUME_LEVEL] = brightness / 100.0
# If the requested entity is a cover, convert to open_cover/close_cover
elif entity.domain == "cover":
domain = entity.domain
if service == SERVICE_TURN_ON:
service = SERVICE_OPEN_COVER
else:
service = SERVICE_CLOSE_COVER
if entity.domain in config.off_maps_to_on_domains: if entity.domain in config.off_maps_to_on_domains:
# Map the off command to on # Map the off command to on
service = SERVICE_TURN_ON service = SERVICE_TURN_ON
@ -206,7 +219,7 @@ class HueOneLightChangeView(HomeAssistantView):
config.cached_states[entity_id] = (result, brightness) config.cached_states[entity_id] = (result, brightness)
# Separate call to turn on needed # Separate call to turn on needed
if domain != core.DOMAIN: if turn_on_needed:
hass.async_add_job(hass.services.async_call( hass.async_add_job(hass.services.async_call(
core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id},
blocking=True)) blocking=True))
@ -288,6 +301,9 @@ def get_entity_state(config, entity):
final_brightness = round(min(1.0, level) * 255) final_brightness = round(min(1.0, level) * 255)
else: else:
final_state, final_brightness = cached_state final_state, final_brightness = cached_state
# Make sure brightness is valid
if final_brightness is None:
final_brightness = 255 if final_state else 0
return (final_state, final_brightness) return (final_state, final_brightness)

View File

@ -12,7 +12,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.components.discovery import load_platform from homeassistant.components.discovery import load_platform
REQUIREMENTS = ['pyenvisalink==1.9', 'pydispatcher==2.0.5'] REQUIREMENTS = ['pyenvisalink==2.0', 'pydispatcher==2.0.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'envisalink' DOMAIN = 'envisalink'

View File

@ -4,6 +4,7 @@ Provides functionality to interact with fans.
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/fan/ https://home-assistant.io/components/fan/
""" """
from datetime import timedelta
import logging import logging
import os import os
@ -21,7 +22,7 @@ import homeassistant.helpers.config_validation as cv
DOMAIN = 'fan' DOMAIN = 'fan'
SCAN_INTERVAL = 30 SCAN_INTERVAL = timedelta(seconds=30)
GROUP_NAME_ALL_FANS = 'all fans' GROUP_NAME_ALL_FANS = 'all fans'
ENTITY_ID_ALL_FANS = group.ENTITY_ID_FORMAT.format(GROUP_NAME_ALL_FANS) ENTITY_ID_ALL_FANS = group.ENTITY_ID_FORMAT.format(GROUP_NAME_ALL_FANS)
@ -32,9 +33,11 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
ATTR_SUPPORTED_FEATURES = 'supported_features' ATTR_SUPPORTED_FEATURES = 'supported_features'
SUPPORT_SET_SPEED = 1 SUPPORT_SET_SPEED = 1
SUPPORT_OSCILLATE = 2 SUPPORT_OSCILLATE = 2
SUPPORT_DIRECTION = 4
SERVICE_SET_SPEED = 'set_speed' SERVICE_SET_SPEED = 'set_speed'
SERVICE_OSCILLATE = 'oscillate' SERVICE_OSCILLATE = 'oscillate'
SERVICE_SET_DIRECTION = 'set_direction'
SPEED_OFF = 'off' SPEED_OFF = 'off'
SPEED_LOW = 'low' SPEED_LOW = 'low'
@ -42,15 +45,20 @@ SPEED_MED = 'med'
SPEED_MEDIUM = 'medium' SPEED_MEDIUM = 'medium'
SPEED_HIGH = 'high' SPEED_HIGH = 'high'
DIRECTION_FORWARD = 'forward'
DIRECTION_REVERSE = 'reverse'
ATTR_SPEED = 'speed' ATTR_SPEED = 'speed'
ATTR_SPEED_LIST = 'speed_list' ATTR_SPEED_LIST = 'speed_list'
ATTR_OSCILLATING = 'oscillating' ATTR_OSCILLATING = 'oscillating'
ATTR_DIRECTION = 'direction'
PROP_TO_ATTR = { PROP_TO_ATTR = {
'speed': ATTR_SPEED, 'speed': ATTR_SPEED,
'speed_list': ATTR_SPEED_LIST, 'speed_list': ATTR_SPEED_LIST,
'oscillating': ATTR_OSCILLATING, 'oscillating': ATTR_OSCILLATING,
'supported_features': ATTR_SUPPORTED_FEATURES, 'supported_features': ATTR_SUPPORTED_FEATURES,
'direction': ATTR_DIRECTION,
} # type: dict } # type: dict
FAN_SET_SPEED_SCHEMA = vol.Schema({ FAN_SET_SPEED_SCHEMA = vol.Schema({
@ -76,6 +84,11 @@ FAN_TOGGLE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids vol.Required(ATTR_ENTITY_ID): cv.entity_ids
}) })
FAN_SET_DIRECTION_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_DIRECTION): cv.string
}) # type: dict
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -140,6 +153,18 @@ def set_speed(hass, entity_id: str=None, speed: str=None) -> None:
hass.services.call(DOMAIN, SERVICE_SET_SPEED, data) hass.services.call(DOMAIN, SERVICE_SET_SPEED, data)
def set_direction(hass, entity_id: str=None, direction: str=None) -> None:
"""Set direction for all or specified fan."""
data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_DIRECTION, direction),
] if value is not None
}
hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, data)
def setup(hass, config: dict) -> None: def setup(hass, config: dict) -> None:
"""Expose fan control via statemachine and services.""" """Expose fan control via statemachine and services."""
component = EntityComponent( component = EntityComponent(
@ -157,7 +182,8 @@ def setup(hass, config: dict) -> None:
service_fun = None service_fun = None
for service_def in [SERVICE_TURN_ON, SERVICE_TURN_OFF, for service_def in [SERVICE_TURN_ON, SERVICE_TURN_OFF,
SERVICE_SET_SPEED, SERVICE_OSCILLATE]: SERVICE_SET_SPEED, SERVICE_OSCILLATE,
SERVICE_SET_DIRECTION]:
if service_def == service.service: if service_def == service.service:
service_fun = service_def service_fun = service_def
break break
@ -190,6 +216,10 @@ def setup(hass, config: dict) -> None:
descriptions.get(SERVICE_OSCILLATE), descriptions.get(SERVICE_OSCILLATE),
schema=FAN_OSCILLATE_SCHEMA) schema=FAN_OSCILLATE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_SET_DIRECTION, handle_fan_service,
descriptions.get(SERVICE_SET_DIRECTION),
schema=FAN_SET_DIRECTION_SCHEMA)
return True return True
@ -200,7 +230,11 @@ class FanEntity(ToggleEntity):
def set_speed(self: ToggleEntity, speed: str) -> None: def set_speed(self: ToggleEntity, speed: str) -> None:
"""Set the speed of the fan.""" """Set the speed of the fan."""
pass raise NotImplementedError()
def set_direction(self: ToggleEntity, direction: str) -> None:
"""Set the direction of the fan."""
raise NotImplementedError()
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None: def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
"""Turn on the fan.""" """Turn on the fan."""
@ -217,14 +251,23 @@ class FanEntity(ToggleEntity):
@property @property
def is_on(self): def is_on(self):
"""Return true if the entity is on.""" """Return true if the entity is on."""
return self.state_attributes.get(ATTR_SPEED, STATE_UNKNOWN) \ return self.speed not in [SPEED_OFF, STATE_UNKNOWN]
not in [SPEED_OFF, STATE_UNKNOWN]
@property
def speed(self) -> str:
"""Return the current speed."""
return None
@property @property
def speed_list(self: ToggleEntity) -> list: def speed_list(self: ToggleEntity) -> list:
"""Get the list of available speeds.""" """Get the list of available speeds."""
return [] return []
@property
def current_direction(self) -> str:
"""Return the current direction of the fan."""
return None
@property @property
def state_attributes(self: ToggleEntity) -> dict: def state_attributes(self: ToggleEntity) -> dict:
"""Return optional state attributes.""" """Return optional state attributes."""

View File

@ -7,14 +7,14 @@ https://home-assistant.io/components/demo/
from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_HIGH, from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_HIGH,
FanEntity, SUPPORT_SET_SPEED, FanEntity, SUPPORT_SET_SPEED,
SUPPORT_OSCILLATE) SUPPORT_OSCILLATE, SUPPORT_DIRECTION)
from homeassistant.const import STATE_OFF from homeassistant.const import STATE_OFF
FAN_NAME = 'Living Room Fan' FAN_NAME = 'Living Room Fan'
FAN_ENTITY_ID = 'fan.living_room_fan' FAN_ENTITY_ID = 'fan.living_room_fan'
DEMO_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE DEMO_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -31,8 +31,9 @@ class DemoFan(FanEntity):
def __init__(self, hass, name: str, initial_state: str) -> None: def __init__(self, hass, name: str, initial_state: str) -> None:
"""Initialize the entity.""" """Initialize the entity."""
self.hass = hass self.hass = hass
self.speed = initial_state self._speed = initial_state
self.oscillating = False self.oscillating = False
self.direction = "forward"
self._name = name self._name = name
@property @property
@ -45,6 +46,11 @@ class DemoFan(FanEntity):
"""No polling needed for a demo fan.""" """No polling needed for a demo fan."""
return False return False
@property
def speed(self) -> str:
"""Return the current speed."""
return self._speed
@property @property
def speed_list(self) -> list: def speed_list(self) -> list:
"""Get the list of available speeds.""" """Get the list of available speeds."""
@ -61,7 +67,12 @@ class DemoFan(FanEntity):
def set_speed(self, speed: str) -> None: def set_speed(self, speed: str) -> None:
"""Set the speed of the fan.""" """Set the speed of the fan."""
self.speed = speed self._speed = speed
self.update_ha_state()
def set_direction(self, direction: str) -> None:
"""Set the direction of the fan."""
self.direction = direction
self.update_ha_state() self.update_ha_state()
def oscillate(self, oscillating: bool) -> None: def oscillate(self, oscillating: bool) -> None:
@ -69,6 +80,11 @@ class DemoFan(FanEntity):
self.oscillating = oscillating self.oscillating = oscillating
self.update_ha_state() self.update_ha_state()
@property
def current_direction(self) -> str:
"""Fan direction."""
return self.direction
@property @property
def supported_features(self) -> int: def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""

View File

@ -64,7 +64,11 @@ class ISYFanDevice(isy.ISYDevice, FanEntity):
def __init__(self, node) -> None: def __init__(self, node) -> None:
"""Initialize the ISY994 fan device.""" """Initialize the ISY994 fan device."""
isy.ISYDevice.__init__(self, node) isy.ISYDevice.__init__(self, node)
self.speed = self.state
@property
def speed(self) -> str:
"""Return the current speed."""
return self.state
@property @property
def state(self) -> str: def state(self) -> str:

View File

@ -0,0 +1,92 @@
"""
Support for Wink fans.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/fan.wink/
"""
import logging
from homeassistant.components.fan import (FanEntity, SPEED_HIGH,
SPEED_LOW, SPEED_MEDIUM,
STATE_UNKNOWN)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.wink import WinkDevice
_LOGGER = logging.getLogger(__name__)
SPEED_LOWEST = "lowest"
SPEED_AUTO = "auto"
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink platform."""
import pywink
add_devices(WinkFanDevice(fan, hass) for fan in pywink.get_fans())
class WinkFanDevice(WinkDevice, FanEntity):
"""Representation of a Wink fan."""
def __init__(self, wink, hass):
"""Initialize the fan."""
WinkDevice.__init__(self, wink, hass)
def set_drection(self: ToggleEntity, direction: str) -> None:
"""Set the direction of the fan."""
self.wink.set_fan_direction(direction)
def set_speed(self: ToggleEntity, speed: str) -> None:
"""Set the speed of the fan."""
self.wink.set_fan_speed(speed)
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
"""Turn on the fan."""
self.wink.set_state(True)
def turn_off(self: ToggleEntity, **kwargs) -> None:
"""Turn off the fan."""
self.wink.set_state(False)
@property
def is_on(self):
"""Return true if the entity is on."""
return self.wink.state()
@property
def speed(self) -> str:
"""Return the current speed."""
current_wink_speed = self.wink.current_fan_speed()
if SPEED_AUTO == current_wink_speed:
return SPEED_AUTO
if SPEED_LOWEST == current_wink_speed:
return SPEED_LOWEST
if SPEED_LOW == current_wink_speed:
return SPEED_LOW
if SPEED_MEDIUM == current_wink_speed:
return SPEED_MEDIUM
if SPEED_HIGH == current_wink_speed:
return SPEED_HIGH
return STATE_UNKNOWN
@property
def current_direction(self):
"""Return direction of the fan [forward, reverse]."""
return self.wink.current_fan_direction()
@property
def speed_list(self: ToggleEntity) -> list:
"""Get the list of available speeds."""
wink_supported_speeds = self.wink.fan_speeds()
supported_speeds = []
if SPEED_AUTO in wink_supported_speeds:
supported_speeds.append(SPEED_AUTO)
if SPEED_LOWEST in wink_supported_speeds:
supported_speeds.append(SPEED_LOWEST)
if SPEED_LOW in wink_supported_speeds:
supported_speeds.append(SPEED_LOW)
if SPEED_MEDIUM in wink_supported_speeds:
supported_speeds.append(SPEED_MEDIUM)
if SPEED_HIGH in wink_supported_speeds:
supported_speeds.append(SPEED_HIGH)
return supported_speeds

View File

@ -8,6 +8,7 @@
<link rel='icon' href='/static/icons/favicon.ico'> <link rel='icon' href='/static/icons/favicon.ico'>
<link rel='apple-touch-icon' sizes='180x180' <link rel='apple-touch-icon' sizes='180x180'
href='/static/icons/favicon-apple-180x180.png'> href='/static/icons/favicon-apple-180x180.png'>
<link rel="mask-icon" href="/static/icons/home-assistant-icon.svg" color="#3fbbf4">
{% for panel in panels.values() -%} {% for panel in panels.values() -%}
<link rel='prefetch' href='{{ panel.url }}'> <link rel='prefetch' href='{{ panel.url }}'>
{% endfor -%} {% endfor -%}

View File

@ -1,18 +1,18 @@
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.""" """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
FINGERPRINTS = { FINGERPRINTS = {
"core.js": "ad1ebcd0614c98a390d982087a7ca75c", "core.js": "22d39af274e1d824ca1302e10971f2d8",
"frontend.html": "826ee6a4b39c939e31aa468b1ef618f9", "frontend.html": "61e57194179b27563a05282b58dd4f47",
"mdi.html": "46a76f877ac9848899b8ed382427c16f", "mdi.html": "5bb2f1717206bad0d187c2633062c575",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e", "panels/ha-panel-dev-event.html": "f19840b9a6a46f57cb064b384e1353f5",
"panels/ha-panel-dev-info.html": "a9c07bf281fe9791fb15827ec1286825", "panels/ha-panel-dev-info.html": "3765a371478cc66d677cf6dcc35267c6",
"panels/ha-panel-dev-service.html": "ac74f7ce66fd7136d25c914ea12f4351", "panels/ha-panel-dev-service.html": "e32bcd3afdf485417a3e20b4fc760776",
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054", "panels/ha-panel-dev-state.html": "8257d99a38358a150eafdb23fa6727e0",
"panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400", "panels/ha-panel-dev-template.html": "cbb251acabd5e7431058ed507b70522b",
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295", "panels/ha-panel-history.html": "7baeb4bd7d9ce0def4f95eab6f10812e",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "4bc5c8370a85a4215413fbae8f85addb", "panels/ha-panel-logbook.html": "93de4cee3a2352a6813b5c218421d534",
"panels/ha-panel-map.html": "3b0ca63286cbe80f27bd36dbc2434e89", "panels/ha-panel-map.html": "3b0ca63286cbe80f27bd36dbc2434e89",
"websocket_test.html": "575de64b431fe11c3785bf96d7813450" "websocket_test.html": "575de64b431fe11c3785bf96d7813450"
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit b5c3575cb5f284178e52d75db24c46131afb4cfa Subproject commit 988ac0028163cfc970e781718bc9459ed486ea61

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:24px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1) <html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:16px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1)
clear: both;white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p>Path to configuration.yaml: [[hassConfigDir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a><a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a><a href="https://github.com/home-assistant/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},hassConfigDir:{type:String,bindNuclear:function(r){return r.configGetters.configDir}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.4.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html> clear: both;white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p>Path to configuration.yaml: [[hassConfigDir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a><a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a><a href="https://github.com/home-assistant/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},hassConfigDir:{type:String,bindNuclear:function(r){return r.configGetters.configDir}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.4.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More