mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
commit
36da5d9adb
22
.coveragerc
22
.coveragerc
@ -13,6 +13,9 @@ omit =
|
||||
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
|
||||
|
||||
@ -34,6 +37,9 @@ omit =
|
||||
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
|
||||
|
||||
@ -157,13 +163,16 @@ omit =
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
homeassistant/components/device_tracker/nmap_tracker.py
|
||||
homeassistant/components/device_tracker/ping.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/device_tracker/swisscom.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/trackr.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
homeassistant/components/device_tracker/volvooncall.py
|
||||
homeassistant/components/device_tracker/xiaomi.py
|
||||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/emoncms_history.py
|
||||
@ -183,8 +192,10 @@ omit =
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/tikteck.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
homeassistant/components/light/zengge.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
@ -202,6 +213,7 @@ omit =
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
homeassistant/components/media_player/mpchc.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/nad.py
|
||||
homeassistant/components/media_player/onkyo.py
|
||||
homeassistant/components/media_player/panasonic_viera.py
|
||||
homeassistant/components/media_player/pandora.py
|
||||
@ -219,12 +231,14 @@ omit =
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/facebook.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/group.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/joaoapps_join.py
|
||||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/lannouncer.py
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
homeassistant/components/notify/matrix.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
@ -254,6 +268,7 @@ omit =
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/cups.py
|
||||
@ -277,6 +292,8 @@ omit =
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hddtemp.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_email_content.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
@ -301,6 +318,7 @@ omit =
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/sma.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/sonarr.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
@ -317,6 +335,7 @@ omit =
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/usps.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
@ -331,6 +350,7 @@ omit =
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/hook.py
|
||||
homeassistant/components/switch/kankun.py
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/netio.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
@ -342,7 +362,9 @@ omit =
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/upnp.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/zeroconf.py
|
||||
|
||||
|
@ -12,6 +12,8 @@ matrix:
|
||||
env: TOXENV=typing
|
||||
- python: "3.5"
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
allow_failures:
|
||||
- python: "3.5"
|
||||
env: TOXENV=typing
|
||||
|
@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot
|
||||
|
||||
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).
|
||||
- Write the code for your device, notification service, sensor, or IoT thing.
|
||||
- Ensure tests work.
|
||||
|
20
Dockerfile
20
Dockerfile
@ -6,24 +6,14 @@ VOLUME /config
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN pip3 install --no-cache-dir colorlog cython
|
||||
|
||||
# For the nmap tracker, bluetooth tracker, Z-Wave, tellstick
|
||||
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
|
||||
# Copy build scripts
|
||||
COPY script/setup_docker_prereqs script/build_python_openzwave script/build_libcec script/
|
||||
RUN script/setup_docker_prereqs
|
||||
|
||||
# Install hass component dependencies
|
||||
COPY requirements_all.txt 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 . .
|
||||
|
@ -26,7 +26,8 @@ Examples of devices Home Assistant can interface with:
|
||||
`Netgear <http://netgear.com>`__,
|
||||
`DD-WRT <http://www.dd-wrt.com/site/index>`__,
|
||||
`TPLink <http://www.tp-link.us/>`__,
|
||||
`ASUSWRT <http://event.asus.com/2013/nw/ASUSWRT/>`__ and any SNMP
|
||||
`ASUSWRT <http://event.asus.com/2013/nw/ASUSWRT/>`__,
|
||||
`Xiaomi <http://miwifi.com/>`__ and any SNMP
|
||||
capable Linksys WAP/WRT
|
||||
- `Philips Hue <http://meethue.com>`__ lights,
|
||||
`WeMo <http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/>`__
|
||||
|
@ -356,6 +356,7 @@ def try_to_restart() -> None:
|
||||
|
||||
def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
if sys.version_info[:3] < (3, 5, 3):
|
||||
monkey_patch_asyncio()
|
||||
|
||||
validate_python()
|
||||
|
@ -395,6 +395,10 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
if not loader.PREPARED:
|
||||
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.
|
||||
# Use OrderedDict in case original one was one.
|
||||
# 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))
|
||||
|
||||
domain_config = config.get(domain, config)
|
||||
message += " (See {}:{}). ".format(
|
||||
message += " (See {}, line {}). ".format(
|
||||
getattr(domain_config, '__config_file__', '?'),
|
||||
getattr(domain_config, '__line__', '?'))
|
||||
|
||||
|
@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
@ -20,7 +21,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
DOMAIN = 'alarm_control_panel'
|
||||
SCAN_INTERVAL = 30
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
ATTR_CHANGED_BY = 'changed_by'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
@ -152,40 +153,48 @@ class AlarmControlPanel(Entity):
|
||||
"""Send disarm command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
"""Send disarm command.
|
||||
|
||||
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)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
"""Send arm home command.
|
||||
|
||||
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)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
"""Send arm away command.
|
||||
|
||||
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)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command."""
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
"""Send alarm trigger command.
|
||||
|
||||
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)
|
||||
|
||||
@property
|
||||
|
@ -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/
|
||||
"""
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import requests
|
||||
@ -25,7 +26,7 @@ DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'CONCORD232'
|
||||
DEFAULT_PORT = 5007
|
||||
|
||||
SCAN_INTERVAL = 1
|
||||
SCAN_INTERVAL = timedelta(seconds=1)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN, CONF_NAME, CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pynx584==0.2']
|
||||
REQUIREMENTS = ['pynx584==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -86,9 +86,11 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
_LOGGER.error('Unable to connect to %(host)s: %(reason)s',
|
||||
dict(host=self._url, reason=ex))
|
||||
self._state = STATE_UNKNOWN
|
||||
zones = []
|
||||
except IndexError:
|
||||
_LOGGER.error('nx584 reports no partitions')
|
||||
self._state = STATE_UNKNOWN
|
||||
zones = []
|
||||
|
||||
bypassed = False
|
||||
for zone in zones:
|
||||
|
@ -203,11 +203,12 @@ class AlexaResponse(object):
|
||||
self.reprompt = None
|
||||
self.session_attributes = {}
|
||||
self.should_end_session = True
|
||||
if intent is not None and 'slots' in intent:
|
||||
self.variables = {key: value['value'] for key, value
|
||||
in intent['slots'].items() if 'value' in value}
|
||||
else:
|
||||
self.variables = {}
|
||||
if intent is not None and 'slots' in intent:
|
||||
for key, value in intent['slots'].items():
|
||||
if 'value' in value:
|
||||
underscored_key = key.replace('.', '_')
|
||||
self.variables[underscored_key] = value['value']
|
||||
|
||||
def add_card(self, card_type, title, content):
|
||||
"""Add a card to the response."""
|
||||
|
@ -133,6 +133,9 @@ class APIEventStream(HomeAssistantView):
|
||||
except asyncio.TimeoutError:
|
||||
yield from to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug('STREAM %s ABORT', id(stop_obj))
|
||||
|
||||
finally:
|
||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||
unsub_stream()
|
||||
|
74
homeassistant/components/bbb_gpio.py
Normal file
74
homeassistant/components/bbb_gpio.py
Normal 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)
|
@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
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
|
||||
|
||||
DOMAIN = 'binary_sensor'
|
||||
SCAN_INTERVAL = 30
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
SENSOR_CLASSES = [
|
||||
|
@ -4,6 +4,7 @@ Support for custom shell commands to retrieve values.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.command_line/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@ -22,7 +23,7 @@ DEFAULT_NAME = 'Binary Command Sensor'
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
|
||||
SCAN_INTERVAL = 60
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COMMAND): cv.string,
|
||||
|
@ -27,7 +27,7 @@ DEFAULT_NAME = 'Alarm'
|
||||
DEFAULT_PORT = '5007'
|
||||
DEFAULT_SSL = False
|
||||
|
||||
SCAN_INTERVAL = 1
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=1)
|
||||
|
||||
ZONE_TYPES_SCHEMA = vol.Schema({
|
||||
cv.positive_int: vol.In(SENSOR_CLASSES),
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Contains functionality to use flic buttons as a binary sensor."""
|
||||
import asyncio
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -10,7 +10,6 @@ from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
|
||||
|
||||
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 async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Setup the flic platform."""
|
||||
import pyflic
|
||||
|
||||
@ -63,26 +60,29 @@ def async_setup_platform(hass, config, async_add_entities,
|
||||
|
||||
def new_button_callback(address):
|
||||
"""Setup newly verified button as device in home assistant."""
|
||||
hass.add_job(async_setup_button(hass, config, async_add_entities,
|
||||
client, address))
|
||||
setup_button(hass, config, add_entities, client, address)
|
||||
|
||||
client.on_new_verified_button = new_button_callback
|
||||
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())
|
||||
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
|
||||
addresses = yield from async_get_verified_addresses(client)
|
||||
if addresses:
|
||||
for address in addresses:
|
||||
yield from async_setup_button(hass, config, async_add_entities,
|
||||
client, address)
|
||||
client.get_info(get_info_callback)
|
||||
|
||||
|
||||
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."""
|
||||
import pyflic
|
||||
|
||||
@ -97,36 +97,20 @@ def start_scanning(hass, config, async_add_entities, client):
|
||||
address, result)
|
||||
|
||||
# Restart scan wizard
|
||||
start_scanning(hass, config, async_add_entities, client)
|
||||
start_scanning(config, add_entities, client)
|
||||
|
||||
scan_wizard.on_completed = scan_completed_callback
|
||||
client.add_scan_wizard(scan_wizard)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_button(hass, config, async_add_entities, client, address):
|
||||
def setup_button(hass, config, add_entities, client, address):
|
||||
"""Setup single button device."""
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
ignored_click_types = config.get(CONF_IGNORED_CLICK_TYPES)
|
||||
button = FlicButton(hass, client, address, timeout, ignored_click_types)
|
||||
_LOGGER.info("Connected to button (%s)", address)
|
||||
|
||||
yield from async_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
|
||||
add_entities([button])
|
||||
|
||||
|
||||
class FlicButton(BinarySensorDevice):
|
||||
|
@ -17,7 +17,7 @@ from homeassistant.const import (
|
||||
CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
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__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
|
@ -47,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
devices = {}
|
||||
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||
map_sv_types, devices, add_devices, MySensorsBinarySensor))
|
||||
map_sv_types, devices, MySensorsBinarySensor, add_devices))
|
||||
|
||||
|
||||
class MySensorsBinarySensor(
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pynx584==0.2']
|
||||
REQUIREMENTS = ['pynx584==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -20,6 +20,8 @@ DEPENDENCIES = []
|
||||
PHILIO = 0x013c
|
||||
PHILIO_SLIM_SENSOR = 0x0002
|
||||
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_SLIM_SENSOR_MOTION = (WENZHOU, PHILIO_SLIM_SENSOR, 0)
|
||||
|
||||
@ -27,6 +29,7 @@ WORKAROUND_NO_OFF_EVENT = 'trigger_no_off_event'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
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,
|
||||
}
|
||||
|
||||
@ -96,6 +99,7 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
_LOGGER.debug('Value changed for label %s', self._value.label)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
|
@ -6,6 +6,8 @@ https://home-assistant.io/components/calendar/
|
||||
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import re
|
||||
|
||||
from homeassistant.components.google import (CONF_OFFSET,
|
||||
@ -20,6 +22,7 @@ from homeassistant.util import dt
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
DOMAIN = 'calendar'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
@ -27,7 +30,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for calendars."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, 60, DOMAIN)
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, DOMAIN)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
@ -144,10 +147,10 @@ class CalendarEventDevice(Entity):
|
||||
def _get_date(date):
|
||||
"""Get the dateTime from date or dateTime as a local."""
|
||||
if 'date' in date:
|
||||
return dt.as_utc(dt.dt.datetime.combine(
|
||||
dt.parse_date(date['date']), dt.dt.time()))
|
||||
return dt.start_of_local_day(dt.dt.datetime.combine(
|
||||
dt.parse_date(date['date']), dt.dt.time.min))
|
||||
else:
|
||||
return dt.parse_datetime(date['dateTime'])
|
||||
return dt.as_local(dt.parse_datetime(date['dateTime']))
|
||||
|
||||
start = _get_date(self.data.event['start'])
|
||||
end = _get_date(self.data.event['end'])
|
||||
|
@ -66,7 +66,7 @@ class GoogleCalendarData(object):
|
||||
"""Get the latest data."""
|
||||
service = self.calendar_service.get()
|
||||
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
|
||||
if self.search:
|
||||
params['q'] = self.search
|
||||
|
@ -6,18 +6,27 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import hashlib
|
||||
|
||||
import aiohttp
|
||||
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_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'camera'
|
||||
DEPENDENCIES = ['http']
|
||||
SCAN_INTERVAL = 30
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
STATE_RECORDING = 'recording'
|
||||
@ -27,11 +36,45 @@ STATE_IDLE = 'idle'
|
||||
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
|
||||
def async_setup(hass, config):
|
||||
"""Setup the camera component."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
hass.http.register_view(CameraImageView(component.entities))
|
||||
hass.http.register_view(CameraMjpegStream(component.entities))
|
||||
@ -46,11 +89,13 @@ class Camera(Entity):
|
||||
def __init__(self):
|
||||
"""Initialize a camera."""
|
||||
self.is_streaming = False
|
||||
self._access_token = hashlib.sha256(
|
||||
str.encode(str(id(self)))).hexdigest()
|
||||
|
||||
@property
|
||||
def access_token(self):
|
||||
"""Access token for this camera."""
|
||||
return str(id(self))
|
||||
return self._access_token
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@ -81,15 +126,12 @@ class Camera(Entity):
|
||||
"""Return bytes of camera image."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""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(
|
||||
None, self.camera_image)
|
||||
return image
|
||||
return self.hass.loop.run_in_executor(None, self.camera_image)
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
@ -131,7 +173,13 @@ class Camera(Entity):
|
||||
yield from response.drain()
|
||||
|
||||
yield from asyncio.sleep(.5)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
response = None
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
@ -201,6 +249,7 @@ class CameraImageView(CameraView):
|
||||
@asyncio.coroutine
|
||||
def handle(self, request, camera):
|
||||
"""Serve camera image."""
|
||||
try:
|
||||
image = yield from camera.async_camera_image()
|
||||
|
||||
if image is None:
|
||||
@ -208,6 +257,9 @@ class CameraImageView(CameraView):
|
||||
|
||||
return web.Response(body=image)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
|
||||
|
||||
class CameraMjpegStream(CameraView):
|
||||
"""Camera View to serve an MJPEG stream."""
|
||||
|
@ -84,8 +84,14 @@ class FFmpegCamera(Camera):
|
||||
if not data:
|
||||
break
|
||||
response.write(data)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
response = None
|
||||
|
||||
finally:
|
||||
self.hass.async_add_job(stream.close())
|
||||
yield from stream.close()
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
|
@ -124,9 +124,13 @@ class MjpegCamera(Camera):
|
||||
except asyncio.TimeoutError:
|
||||
raise HTTPGatewayTimeout()
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
response = None
|
||||
|
||||
finally:
|
||||
if stream is not None:
|
||||
self.hass.async_add_job(stream.release())
|
||||
stream.close()
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
|
||||
|
@ -276,9 +276,13 @@ class SynologyCamera(Camera):
|
||||
_LOGGER.exception("Error on %s", streaming_url)
|
||||
raise HTTPGatewayTimeout()
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
response = None
|
||||
|
||||
finally:
|
||||
if stream is not None:
|
||||
self.hass.async_add_job(stream.release())
|
||||
stream.close()
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.const import CONF_PORT
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['uvcclient==0.9.0']
|
||||
REQUIREMENTS = ['uvcclient==0.10.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -4,15 +4,18 @@ Provides functionality to interact with climate devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
import functools as ft
|
||||
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.util.temperature import convert as convert_temperature
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -23,7 +26,7 @@ from homeassistant.const import (
|
||||
DOMAIN = "climate"
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
SCAN_INTERVAL = 60
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
SERVICE_SET_AWAY_MODE = "set_away_mode"
|
||||
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)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup climate devices."""
|
||||
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'))
|
||||
|
||||
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."""
|
||||
target_climate = component.extract_from_service(service)
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
away_mode = service.data.get(ATTR_AWAY_MODE)
|
||||
|
||||
@ -207,21 +231,21 @@ def setup(hass, config):
|
||||
|
||||
for climate in target_climate:
|
||||
if away_mode:
|
||||
climate.turn_away_mode_on()
|
||||
yield from climate.async_turn_away_mode_on()
|
||||
else:
|
||||
climate.turn_away_mode_off()
|
||||
yield from climate.async_turn_away_mode_off()
|
||||
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
yield from _async_update_climate(target_climate)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service,
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service,
|
||||
descriptions.get(SERVICE_SET_AWAY_MODE),
|
||||
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."""
|
||||
target_climate = component.extract_from_service(service)
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
aux_heat = service.data.get(ATTR_AUX_HEAT)
|
||||
|
||||
@ -233,21 +257,21 @@ def setup(hass, config):
|
||||
|
||||
for climate in target_climate:
|
||||
if aux_heat:
|
||||
climate.turn_aux_heat_on()
|
||||
yield from climate.async_turn_aux_heat_on()
|
||||
else:
|
||||
climate.turn_aux_heat_off()
|
||||
yield from climate.async_turn_aux_heat_off()
|
||||
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
yield from _async_update_climate(target_climate)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_AUX_HEAT, aux_heat_set_service,
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service,
|
||||
descriptions.get(SERVICE_SET_AUX_HEAT),
|
||||
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."""
|
||||
target_climate = component.extract_from_service(service)
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
for climate in target_climate:
|
||||
kwargs = {}
|
||||
@ -261,18 +285,19 @@ def setup(hass, config):
|
||||
else:
|
||||
kwargs[value] = temp
|
||||
|
||||
climate.set_temperature(**kwargs)
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
yield from climate.async_set_temperature(**kwargs)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service,
|
||||
yield from _async_update_climate(target_climate)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service,
|
||||
descriptions.get(SERVICE_SET_TEMPERATURE),
|
||||
schema=SET_TEMPERATURE_SCHEMA)
|
||||
|
||||
def humidity_set_service(service):
|
||||
@asyncio.coroutine
|
||||
def async_humidity_set_service(service):
|
||||
"""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)
|
||||
|
||||
@ -283,19 +308,19 @@ def setup(hass, config):
|
||||
return
|
||||
|
||||
for climate in target_climate:
|
||||
climate.set_humidity(humidity)
|
||||
yield from climate.async_set_humidity(humidity)
|
||||
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
yield from _async_update_climate(target_climate)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_HUMIDITY, humidity_set_service,
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service,
|
||||
descriptions.get(SERVICE_SET_HUMIDITY),
|
||||
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."""
|
||||
target_climate = component.extract_from_service(service)
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
fan = service.data.get(ATTR_FAN_MODE)
|
||||
|
||||
@ -306,19 +331,19 @@ def setup(hass, config):
|
||||
return
|
||||
|
||||
for climate in target_climate:
|
||||
climate.set_fan_mode(fan)
|
||||
yield from climate.async_set_fan_mode(fan)
|
||||
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
yield from _async_update_climate(target_climate)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service,
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service,
|
||||
descriptions.get(SERVICE_SET_FAN_MODE),
|
||||
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."""
|
||||
target_climate = component.extract_from_service(service)
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
operation_mode = service.data.get(ATTR_OPERATION_MODE)
|
||||
|
||||
@ -329,19 +354,19 @@ def setup(hass, config):
|
||||
return
|
||||
|
||||
for climate in target_climate:
|
||||
climate.set_operation_mode(operation_mode)
|
||||
yield from climate.async_set_operation_mode(operation_mode)
|
||||
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
yield from _async_update_climate(target_climate)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_OPERATION_MODE, operation_set_service,
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service,
|
||||
descriptions.get(SERVICE_SET_OPERATION_MODE),
|
||||
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."""
|
||||
target_climate = component.extract_from_service(service)
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
swing_mode = service.data.get(ATTR_SWING_MODE)
|
||||
|
||||
@ -352,15 +377,15 @@ def setup(hass, config):
|
||||
return
|
||||
|
||||
for climate in target_climate:
|
||||
climate.set_swing_mode(swing_mode)
|
||||
yield from climate.async_set_swing_mode(swing_mode)
|
||||
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
yield from _async_update_climate(target_climate)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_SWING_MODE, swing_set_service,
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service,
|
||||
descriptions.get(SERVICE_SET_SWING_MODE),
|
||||
schema=SET_SWING_MODE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -521,38 +546,110 @@ class ClimateDevice(Entity):
|
||||
"""Set new target temperature."""
|
||||
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):
|
||||
"""Set new target humidity."""
|
||||
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):
|
||||
"""Set new target fan mode."""
|
||||
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):
|
||||
"""Set new target operation mode."""
|
||||
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):
|
||||
"""Set new target swing operation."""
|
||||
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):
|
||||
"""Turn away mode on."""
|
||||
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):
|
||||
"""Turn away mode off."""
|
||||
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):
|
||||
"""Turn auxillary heater on."""
|
||||
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):
|
||||
"""Turn auxillary heater off."""
|
||||
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
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
|
@ -22,16 +22,25 @@ _CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_FAN_MIN_ON_TIME = 'fan_min_on_time'
|
||||
ATTR_RESUME_ALL = 'resume_all'
|
||||
|
||||
DEFAULT_RESUME_ALL = False
|
||||
|
||||
DEPENDENCIES = ['ecobee']
|
||||
|
||||
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({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
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):
|
||||
"""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):
|
||||
"""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:
|
||||
target_thermostats = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
if device.entity_id in entity_id]
|
||||
else:
|
||||
target_thermostats = devices
|
||||
|
||||
fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME]
|
||||
|
||||
for thermostat in target_thermostats:
|
||||
thermostat.set_fan_min_on_time(str(fan_min_on_time))
|
||||
|
||||
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(
|
||||
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),
|
||||
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):
|
||||
"""A thermostat class for Ecobee."""
|
||||
@ -249,6 +278,12 @@ class Thermostat(ClimateDevice):
|
||||
fan_min_on_time)
|
||||
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:
|
||||
|
||||
# def turn_home_mode_on(self):
|
||||
|
@ -8,18 +8,27 @@ import logging
|
||||
|
||||
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 (
|
||||
CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE)
|
||||
from homeassistant.util.temperature import convert
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['bluepy_devices==0.2.0']
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_MODE = 'mode'
|
||||
ATTR_MODE_READABLE = 'mode_readable'
|
||||
STATE_BOOST = "boost"
|
||||
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({
|
||||
vol.Required(CONF_MAC): cv.string,
|
||||
@ -48,10 +57,23 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
|
||||
def __init__(self, _mac, _name):
|
||||
"""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._thermostat = eq3btsmart.EQ3BTSmartThermostat(_mac)
|
||||
self._thermostat = eq3.Thermostat(_mac)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -63,6 +85,11 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
"""Return the unit of measurement that is used."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return eq3bt's precision 0.5."""
|
||||
return PRECISION_HALVES
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Can not report temperature, so return target_temperature."""
|
||||
@ -81,24 +108,53 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
self._thermostat.target_temperature = temperature
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {
|
||||
ATTR_MODE: self._thermostat.mode,
|
||||
ATTR_MODE_READABLE: self._thermostat.mode_readable,
|
||||
}
|
||||
def current_operation(self):
|
||||
"""Current mode."""
|
||||
return self.modes[self._thermostat.mode]
|
||||
|
||||
@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
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return convert(self._thermostat.min_temp, TEMP_CELSIUS,
|
||||
self.unit_of_measurement)
|
||||
return self._thermostat.min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return convert(self._thermostat.max_temp, TEMP_CELSIUS,
|
||||
self.unit_of_measurement)
|
||||
return self._thermostat.max_temp
|
||||
|
||||
@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):
|
||||
"""Update the data from the thermostat."""
|
||||
|
@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
}
|
||||
devices = {}
|
||||
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):
|
||||
|
@ -76,7 +76,7 @@ set_operation_mode:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
example: 'climet.nest'
|
||||
example: 'climate.nest'
|
||||
|
||||
operation_mode:
|
||||
description: New value of operation mode
|
||||
@ -94,3 +94,27 @@ set_swing_mode:
|
||||
swing_mode:
|
||||
description: New value of swing mode
|
||||
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
|
||||
|
@ -89,9 +89,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
_LOGGER.debug('Value changed for label %s', self._value.label)
|
||||
self.update_properties()
|
||||
self.schedule_update_ha_state()
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
|
@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover/
|
||||
"""
|
||||
import os
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@ -23,7 +24,7 @@ from homeassistant.const import (
|
||||
|
||||
|
||||
DOMAIN = 'cover'
|
||||
SCAN_INTERVAL = 15
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
GROUP_NAME_ALL_COVERS = 'all covers'
|
||||
ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers')
|
||||
|
@ -35,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
})
|
||||
devices = {}
|
||||
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):
|
||||
|
@ -78,9 +78,9 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
_LOGGER.debug('Value changed for label %s', self._value.label)
|
||||
self.update_properties()
|
||||
self.update_ha_state()
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
@ -170,9 +170,9 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
_LOGGER.debug('Value changed for label %s', self._value.label)
|
||||
self._state = value.data
|
||||
self.update_ha_state()
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
|
@ -23,12 +23,14 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
'cover',
|
||||
'device_tracker',
|
||||
'fan',
|
||||
'image_processing',
|
||||
'light',
|
||||
'lock',
|
||||
'media_player',
|
||||
'notify',
|
||||
'sensor',
|
||||
'switch',
|
||||
'tts',
|
||||
]
|
||||
|
||||
|
||||
|
@ -8,7 +8,7 @@ import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Sequence, Callable
|
||||
from typing import Any, List, Sequence, Callable
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
@ -24,6 +24,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import config_per_platform, discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util as util
|
||||
@ -50,10 +51,10 @@ CONF_TRACK_NEW = 'track_new_devices'
|
||||
DEFAULT_TRACK_NEW = True
|
||||
|
||||
CONF_CONSIDER_HOME = 'consider_home'
|
||||
DEFAULT_CONSIDER_HOME = 180
|
||||
DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
|
||||
|
||||
CONF_SCAN_INTERVAL = 'interval_seconds'
|
||||
DEFAULT_SCAN_INTERVAL = 12
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=12)
|
||||
|
||||
CONF_AWAY_HIDE = 'hide_if_away'
|
||||
DEFAULT_AWAY_HIDE = False
|
||||
@ -69,12 +70,16 @@ ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_GPS = 'gps'
|
||||
ATTR_BATTERY = 'battery'
|
||||
ATTR_ATTRIBUTES = 'attributes'
|
||||
ATTR_SOURCE_TYPE = 'source_type'
|
||||
|
||||
SOURCE_TYPE_GPS = 'gps'
|
||||
SOURCE_TYPE_ROUTER = 'router'
|
||||
|
||||
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_CONSIDER_HOME,
|
||||
default=timedelta(seconds=DEFAULT_CONSIDER_HOME)): vol.All(
|
||||
default=DEFAULT_CONSIDER_HOME): vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
})
|
||||
|
||||
@ -121,8 +126,7 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
return False
|
||||
else:
|
||||
conf = conf[0] if len(conf) > 0 else {}
|
||||
consider_home = conf.get(CONF_CONSIDER_HOME,
|
||||
timedelta(seconds=DEFAULT_CONSIDER_HOME))
|
||||
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
|
||||
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
|
||||
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:
|
||||
return
|
||||
|
||||
_LOGGER.info("Setting up %s.%s", DOMAIN, p_type)
|
||||
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(
|
||||
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:
|
||||
_LOGGER.error('Error setting up platform %s', p_type)
|
||||
return
|
||||
|
||||
if scanner:
|
||||
yield from async_setup_scanner_platform(
|
||||
hass, p_config, scanner, tracker.async_see)
|
||||
return
|
||||
|
||||
ret = yield from hass.loop.run_in_executor(
|
||||
None, platform.setup_scanner, hass, p_config, tracker.see)
|
||||
if not ret:
|
||||
if not setup:
|
||||
_LOGGER.error('Error setting up platform %s', p_type)
|
||||
return
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_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,
|
||||
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."""
|
||||
self.hass.add_job(
|
||||
self.async_see(mac, dev_id, host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
gps_accuracy, battery, attributes, source_type)
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_see(self, mac: str=None, dev_id: str=None, host_name: str=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.
|
||||
|
||||
This method is a coroutine.
|
||||
@ -251,7 +268,8 @@ class DeviceTracker(object):
|
||||
|
||||
if device:
|
||||
yield from device.async_seen(host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
gps_accuracy, battery, attributes,
|
||||
source_type)
|
||||
if device.track:
|
||||
yield from device.async_update_ha_state()
|
||||
return
|
||||
@ -266,7 +284,8 @@ class DeviceTracker(object):
|
||||
self.mac_to_dev[mac] = device
|
||||
|
||||
yield from device.async_seen(host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
gps_accuracy, battery, attributes,
|
||||
source_type)
|
||||
|
||||
if device.track:
|
||||
yield from device.async_update_ha_state()
|
||||
@ -370,6 +389,9 @@ class Device(Entity):
|
||||
|
||||
self.away_hide = hide_if_away
|
||||
self.vendor = vendor
|
||||
|
||||
self.source_type = None
|
||||
|
||||
self._attributes = {}
|
||||
|
||||
@property
|
||||
@ -390,7 +412,9 @@ class Device(Entity):
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
attr = {}
|
||||
attr = {
|
||||
ATTR_SOURCE_TYPE: self.source_type
|
||||
}
|
||||
|
||||
if self.gps:
|
||||
attr[ATTR_LATITUDE] = self.gps[0]
|
||||
@ -415,12 +439,13 @@ class Device(Entity):
|
||||
@asyncio.coroutine
|
||||
def async_seen(self, host_name: str=None, location_name: 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."""
|
||||
self.source_type = source_type
|
||||
self.last_seen = dt_util.utcnow()
|
||||
self.host_name = host_name
|
||||
self.location_name = location_name
|
||||
self.gps_accuracy = gps_accuracy or 0
|
||||
|
||||
if battery:
|
||||
self.battery = battery
|
||||
if attributes:
|
||||
@ -431,7 +456,10 @@ class Device(Entity):
|
||||
if gps is not None:
|
||||
try:
|
||||
self.gps = float(gps[0]), float(gps[1])
|
||||
self.gps_accuracy = gps_accuracy or 0
|
||||
except (ValueError, TypeError, IndexError):
|
||||
self.gps = None
|
||||
self.gps_accuracy = 0
|
||||
_LOGGER.warning('Could not parse gps value for %s: %s',
|
||||
self.dev_id, gps)
|
||||
|
||||
@ -456,7 +484,7 @@ class Device(Entity):
|
||||
return
|
||||
elif 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(
|
||||
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
|
||||
if zone_state is None:
|
||||
@ -465,9 +493,9 @@ class Device(Entity):
|
||||
self._state = STATE_HOME
|
||||
else:
|
||||
self._state = zone_state.name
|
||||
|
||||
elif self.stale():
|
||||
self._state = STATE_NOT_HOME
|
||||
self.gps = None
|
||||
self.last_update_home = False
|
||||
else:
|
||||
self._state = STATE_HOME
|
||||
@ -485,13 +513,18 @@ class Device(Entity):
|
||||
if not self.mac:
|
||||
return None
|
||||
|
||||
if '_' in self.mac:
|
||||
_, mac = self.mac.split('_', 1)
|
||||
else:
|
||||
mac = self.mac
|
||||
|
||||
# prevent lookup of invalid macs
|
||||
if not len(self.mac.split(':')) == 6:
|
||||
if not len(mac.split(':')) == 6:
|
||||
return 'unknown'
|
||||
|
||||
# we only need the first 3 bytes of the mac for a lookup
|
||||
# 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
|
||||
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
|
||||
url = 'http://api.macvendors.com/' + oui
|
||||
@ -521,6 +554,34 @@ class Device(Entity):
|
||||
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):
|
||||
"""Load devices from YAML configuration file."""
|
||||
return run_coroutine_threadsafe(
|
||||
@ -577,26 +638,39 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
This method is a coroutine.
|
||||
"""
|
||||
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
|
||||
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."""
|
||||
found_devices = scanner.scan_devices()
|
||||
found_devices = yield from scanner.async_scan_devices()
|
||||
|
||||
for mac in found_devices:
|
||||
if mac in seen:
|
||||
host_name = None
|
||||
else:
|
||||
host_name = scanner.get_device_name(mac)
|
||||
host_name = yield from scanner.async_get_device_name(mac)
|
||||
seen.add(mac)
|
||||
hass.add_job(async_see_device(mac=mac, host_name=host_name))
|
||||
|
||||
async_track_utc_time_change(
|
||||
hass, device_tracker_scan, second=range(0, 60, interval))
|
||||
kwargs = {
|
||||
'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):
|
||||
|
@ -14,7 +14,8 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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.util import Throttle
|
||||
|
||||
@ -46,7 +47,7 @@ def get_scanner(hass, config):
|
||||
Device = namedtuple("Device", ["mac", "ip", "last_update"])
|
||||
|
||||
|
||||
class ActiontecDeviceScanner(object):
|
||||
class ActiontecDeviceScanner(DeviceScanner):
|
||||
"""This class queries a an actiontec router for connected devices."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
@ -12,7 +12,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
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.util import Throttle
|
||||
|
||||
@ -42,7 +43,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class ArubaDeviceScanner(object):
|
||||
class ArubaDeviceScanner(DeviceScanner):
|
||||
"""This class queries a Aruba Access Point for connected devices."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
@ -14,7 +14,8 @@ from datetime import timedelta
|
||||
|
||||
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.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -97,7 +98,7 @@ def get_scanner(hass, config):
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram')
|
||||
|
||||
|
||||
class AsusWrtDeviceScanner(object):
|
||||
class AsusWrtDeviceScanner(DeviceScanner):
|
||||
"""This class queries a router running ASUSWRT firmware."""
|
||||
|
||||
# Eighth attribute needed for mode (AP mode vs router mode)
|
||||
|
@ -11,8 +11,8 @@ import requests
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (PLATFORM_SCHEMA,
|
||||
ATTR_ATTRIBUTES)
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, ATTR_ATTRIBUTES)
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
|
@ -9,7 +9,7 @@ import logging
|
||||
from datetime import timedelta
|
||||
|
||||
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
|
||||
|
||||
REQUIREMENTS = ['pybbox==0.0.5-alpha']
|
||||
@ -29,7 +29,7 @@ def get_scanner(hass, config):
|
||||
Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update'])
|
||||
|
||||
|
||||
class BboxDeviceScanner(object):
|
||||
class BboxDeviceScanner(DeviceScanner):
|
||||
"""This class scans for devices connected to the bbox."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
@ -5,13 +5,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.components.device_tracker import (
|
||||
YAML_DEVICES,
|
||||
CONF_TRACK_NEW,
|
||||
CONF_SCAN_INTERVAL,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
PLATFORM_SCHEMA,
|
||||
load_config,
|
||||
DEFAULT_TRACK_NEW
|
||||
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
|
||||
PLATFORM_SCHEMA, load_config, DEFAULT_TRACK_NEW
|
||||
)
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.dt as dt_util
|
||||
@ -24,9 +19,11 @@ REQUIREMENTS = ['gattlib==0.20150805']
|
||||
BLE_PREFIX = 'BLE_'
|
||||
MIN_SEEN_NEW = 5
|
||||
CONF_SCAN_DURATION = "scan_duration"
|
||||
CONF_BLUETOOTH_DEVICE = "device_id"
|
||||
|
||||
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."""
|
||||
_LOGGER.debug("Discovering Bluetooth LE devices")
|
||||
try:
|
||||
service = DiscoveryService()
|
||||
service = DiscoveryService(ble_dev_id)
|
||||
devices = service.discover(duration)
|
||||
_LOGGER.debug("Bluetooth LE devices discovered = %s", devices)
|
||||
except RuntimeError as error:
|
||||
@ -70,6 +67,7 @@ def setup_scanner(hass, config, see):
|
||||
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
duration = config.get(CONF_SCAN_DURATION)
|
||||
ble_dev_id = config.get(CONF_BLUETOOTH_DEVICE)
|
||||
devs_to_track = []
|
||||
devs_donot_track = []
|
||||
|
||||
|
@ -16,7 +16,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
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.util import Throttle
|
||||
|
||||
@ -40,7 +41,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class BTHomeHub5DeviceScanner(object):
|
||||
class BTHomeHub5DeviceScanner(DeviceScanner):
|
||||
"""This class queries a BT Home Hub 5."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
@ -10,7 +10,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
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, \
|
||||
CONF_PORT
|
||||
from homeassistant.util import Throttle
|
||||
@ -39,7 +40,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class CiscoDeviceScanner(object):
|
||||
class CiscoDeviceScanner(DeviceScanner):
|
||||
"""This class queries a wireless router running Cisco IOS firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
@ -13,7 +13,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
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.util import Throttle
|
||||
|
||||
@ -41,7 +42,7 @@ def get_scanner(hass, config):
|
||||
return None
|
||||
|
||||
|
||||
class DdWrtDeviceScanner(object):
|
||||
class DdWrtDeviceScanner(DeviceScanner):
|
||||
"""This class queries a wireless router running DD-WRT firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
@ -10,7 +10,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
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.util import Throttle
|
||||
|
||||
@ -38,7 +39,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class FritzBoxScanner(object):
|
||||
class FritzBoxScanner(DeviceScanner):
|
||||
"""This class queries a FRITZ!Box router."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
@ -12,7 +12,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
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.helpers.event import track_utc_time_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -131,7 +131,7 @@ def setup_scanner(hass, config: dict, see):
|
||||
return True
|
||||
|
||||
|
||||
class Icloud(object):
|
||||
class Icloud(DeviceScanner):
|
||||
"""Represent an icloud account in Home Assistant."""
|
||||
|
||||
def __init__(self, hass, username, password, name, see):
|
||||
|
@ -8,9 +8,8 @@ import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
STATE_NOT_HOME,
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE, ATTR_LONGITUDE, STATE_NOT_HOME, HTTP_UNPROCESSABLE_ENTITY)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
# pylint: disable=unused-import
|
||||
from homeassistant.components.device_tracker import ( # NOQA
|
||||
@ -64,18 +63,18 @@ class LocativeView(HomeAssistantView):
|
||||
return ('Device id not specified.',
|
||||
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:
|
||||
_LOGGER.error('Trigger is not specified.')
|
||||
return ('Trigger is not specified.',
|
||||
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('-', '')
|
||||
location_name = data['id'].lower()
|
||||
location_name = data.get('id', data['trigger']).lower()
|
||||
direction = data['trigger']
|
||||
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
|
||||
|
||||
|
@ -14,7 +14,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
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.util import Throttle
|
||||
|
||||
@ -37,7 +38,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class LuciDeviceScanner(object):
|
||||
class LuciDeviceScanner(DeviceScanner):
|
||||
"""This class queries a wireless router running OpenWrt firmware.
|
||||
|
||||
Adapted from Tomato scanner.
|
||||
|
@ -11,7 +11,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
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, CONF_PORT)
|
||||
from homeassistant.util import Throttle
|
||||
@ -47,7 +48,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class NetgearDeviceScanner(object):
|
||||
class NetgearDeviceScanner(DeviceScanner):
|
||||
"""Queries a Netgear wireless router using the SOAP-API."""
|
||||
|
||||
def __init__(self, host, username, password, port):
|
||||
|
@ -14,7 +14,8 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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.util import Throttle
|
||||
|
||||
@ -63,7 +64,7 @@ def _arp(ip_address):
|
||||
return None
|
||||
|
||||
|
||||
class NmapDeviceScanner(object):
|
||||
class NmapDeviceScanner(DeviceScanner):
|
||||
"""This class scans for devices using nmap."""
|
||||
|
||||
exclude = []
|
||||
|
92
homeassistant/components/device_tracker/ping.py
Normal file
92
homeassistant/components/device_tracker/ping.py
Normal 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())
|
@ -12,7 +12,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
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.util import Throttle
|
||||
|
||||
@ -46,7 +47,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class SnmpScanner(object):
|
||||
class SnmpScanner(DeviceScanner):
|
||||
"""Queries any SNMP capable Access Point for connected devices."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
@ -12,7 +12,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
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.util import Throttle
|
||||
|
||||
@ -35,7 +36,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class SwisscomDeviceScanner(object):
|
||||
class SwisscomDeviceScanner(DeviceScanner):
|
||||
"""This class queries a router running Swisscom Internet-Box firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
@ -13,7 +13,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
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.util import Throttle
|
||||
|
||||
@ -46,7 +47,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class ThomsonDeviceScanner(object):
|
||||
class ThomsonDeviceScanner(DeviceScanner):
|
||||
"""This class queries a router running THOMSON firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
@ -14,7 +14,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
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.util import Throttle
|
||||
|
||||
@ -38,7 +39,7 @@ def get_scanner(hass, config):
|
||||
return TomatoDeviceScanner(config[DOMAIN])
|
||||
|
||||
|
||||
class TomatoDeviceScanner(object):
|
||||
class TomatoDeviceScanner(DeviceScanner):
|
||||
"""This class queries a wireless router running Tomato firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
@ -15,7 +15,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
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.util import Throttle
|
||||
|
||||
@ -42,7 +43,7 @@ def get_scanner(hass, config):
|
||||
return None
|
||||
|
||||
|
||||
class TplinkDeviceScanner(object):
|
||||
class TplinkDeviceScanner(DeviceScanner):
|
||||
"""This class queries a wireless router running TP-Link firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
79
homeassistant/components/device_tracker/trackr.py
Normal file
79
homeassistant/components/device_tracker/trackr.py
Normal 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
|
||||
)
|
@ -14,7 +14,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
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.util import Throttle
|
||||
|
||||
@ -37,7 +38,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class UbusDeviceScanner(object):
|
||||
class UbusDeviceScanner(DeviceScanner):
|
||||
"""
|
||||
This class queries a wireless router running OpenWrt firmware.
|
||||
|
||||
|
@ -10,7 +10,8 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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
|
||||
|
||||
# Unifi package doesn't list urllib3 as a requirement
|
||||
@ -59,7 +60,7 @@ def get_scanner(hass, config):
|
||||
return UnifiScanner(ctrl)
|
||||
|
||||
|
||||
class UnifiScanner(object):
|
||||
class UnifiScanner(DeviceScanner):
|
||||
"""Provide device_tracker support from Unifi WAP client data."""
|
||||
|
||||
def __init__(self, controller):
|
||||
|
164
homeassistant/components/device_tracker/upc_connect.py
Normal file
164
homeassistant/components/device_tracker/upc_connect.py
Normal 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')
|
@ -14,12 +14,9 @@ from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_USERNAME)
|
||||
CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME)
|
||||
from homeassistant.components.device_tracker import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
PLATFORM_SCHEMA)
|
||||
DEFAULT_SCAN_INTERVAL, PLATFORM_SCHEMA)
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(minutes=1)
|
||||
|
||||
|
145
homeassistant/components/device_tracker/xiaomi.py
Normal file
145
homeassistant/components/device_tracker/xiaomi.py
Normal 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)
|
@ -38,6 +38,7 @@ SERVICE_HANDLERS = {
|
||||
'directv': ('media_player', 'directv'),
|
||||
'denonavr': ('media_player', 'denonavr'),
|
||||
'samsung_tv': ('media_player', 'samsungtv'),
|
||||
'yeelight': ('light', 'yeelight'),
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
|
@ -7,7 +7,8 @@ from aiohttp import web
|
||||
from homeassistant import core
|
||||
from homeassistant.const import (
|
||||
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 (
|
||||
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
||||
@ -90,7 +91,7 @@ class HueOneLightStateView(HomeAssistantView):
|
||||
self.config = config
|
||||
|
||||
@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."""
|
||||
hass = request.app['hass']
|
||||
entity_id = self.config.number_to_entity_id(entity_id)
|
||||
@ -161,6 +162,9 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
# Choose general HA 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
|
||||
service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
|
||||
|
||||
@ -189,11 +193,20 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
ATTR_SUPPORTED_MEDIA_COMMANDS, 0)
|
||||
if media_commands & SUPPORT_VOLUME_SET == SUPPORT_VOLUME_SET:
|
||||
if brightness is not None:
|
||||
turn_on_needed = True
|
||||
domain = entity.domain
|
||||
service = SERVICE_VOLUME_SET
|
||||
# Convert 0-100 to 0.0-1.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:
|
||||
# Map the off command to on
|
||||
service = SERVICE_TURN_ON
|
||||
@ -206,7 +219,7 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
config.cached_states[entity_id] = (result, brightness)
|
||||
|
||||
# Separate call to turn on needed
|
||||
if domain != core.DOMAIN:
|
||||
if turn_on_needed:
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True))
|
||||
@ -288,6 +301,9 @@ def get_entity_state(config, entity):
|
||||
final_brightness = round(min(1.0, level) * 255)
|
||||
else:
|
||||
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)
|
||||
|
||||
|
@ -12,7 +12,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
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__)
|
||||
DOMAIN = 'envisalink'
|
||||
|
@ -4,6 +4,7 @@ Provides functionality to interact with fans.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/fan/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
@ -21,7 +22,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
DOMAIN = 'fan'
|
||||
SCAN_INTERVAL = 30
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
GROUP_NAME_ALL_FANS = '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'
|
||||
SUPPORT_SET_SPEED = 1
|
||||
SUPPORT_OSCILLATE = 2
|
||||
SUPPORT_DIRECTION = 4
|
||||
|
||||
SERVICE_SET_SPEED = 'set_speed'
|
||||
SERVICE_OSCILLATE = 'oscillate'
|
||||
SERVICE_SET_DIRECTION = 'set_direction'
|
||||
|
||||
SPEED_OFF = 'off'
|
||||
SPEED_LOW = 'low'
|
||||
@ -42,15 +45,20 @@ SPEED_MED = 'med'
|
||||
SPEED_MEDIUM = 'medium'
|
||||
SPEED_HIGH = 'high'
|
||||
|
||||
DIRECTION_FORWARD = 'forward'
|
||||
DIRECTION_REVERSE = 'reverse'
|
||||
|
||||
ATTR_SPEED = 'speed'
|
||||
ATTR_SPEED_LIST = 'speed_list'
|
||||
ATTR_OSCILLATING = 'oscillating'
|
||||
ATTR_DIRECTION = 'direction'
|
||||
|
||||
PROP_TO_ATTR = {
|
||||
'speed': ATTR_SPEED,
|
||||
'speed_list': ATTR_SPEED_LIST,
|
||||
'oscillating': ATTR_OSCILLATING,
|
||||
'supported_features': ATTR_SUPPORTED_FEATURES,
|
||||
'direction': ATTR_DIRECTION,
|
||||
} # type: dict
|
||||
|
||||
FAN_SET_SPEED_SCHEMA = vol.Schema({
|
||||
@ -76,6 +84,11 @@ FAN_TOGGLE_SCHEMA = vol.Schema({
|
||||
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__)
|
||||
|
||||
|
||||
@ -140,6 +153,18 @@ def set_speed(hass, entity_id: str=None, speed: str=None) -> None:
|
||||
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:
|
||||
"""Expose fan control via statemachine and services."""
|
||||
component = EntityComponent(
|
||||
@ -157,7 +182,8 @@ def setup(hass, config: dict) -> None:
|
||||
|
||||
service_fun = None
|
||||
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:
|
||||
service_fun = service_def
|
||||
break
|
||||
@ -190,6 +216,10 @@ def setup(hass, config: dict) -> None:
|
||||
descriptions.get(SERVICE_OSCILLATE),
|
||||
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
|
||||
|
||||
|
||||
@ -200,7 +230,11 @@ class FanEntity(ToggleEntity):
|
||||
|
||||
def set_speed(self: ToggleEntity, speed: str) -> None:
|
||||
"""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:
|
||||
"""Turn on the fan."""
|
||||
@ -217,14 +251,23 @@ class FanEntity(ToggleEntity):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the entity is on."""
|
||||
return self.state_attributes.get(ATTR_SPEED, STATE_UNKNOWN) \
|
||||
not in [SPEED_OFF, STATE_UNKNOWN]
|
||||
return self.speed not in [SPEED_OFF, STATE_UNKNOWN]
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def speed_list(self: ToggleEntity) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
return []
|
||||
|
||||
@property
|
||||
def current_direction(self) -> str:
|
||||
"""Return the current direction of the fan."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_attributes(self: ToggleEntity) -> dict:
|
||||
"""Return optional state attributes."""
|
||||
|
@ -7,14 +7,14 @@ https://home-assistant.io/components/demo/
|
||||
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_HIGH,
|
||||
FanEntity, SUPPORT_SET_SPEED,
|
||||
SUPPORT_OSCILLATE)
|
||||
SUPPORT_OSCILLATE, SUPPORT_DIRECTION)
|
||||
from homeassistant.const import STATE_OFF
|
||||
|
||||
|
||||
FAN_NAME = '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
|
||||
@ -31,8 +31,9 @@ class DemoFan(FanEntity):
|
||||
def __init__(self, hass, name: str, initial_state: str) -> None:
|
||||
"""Initialize the entity."""
|
||||
self.hass = hass
|
||||
self.speed = initial_state
|
||||
self._speed = initial_state
|
||||
self.oscillating = False
|
||||
self.direction = "forward"
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
@ -45,6 +46,11 @@ class DemoFan(FanEntity):
|
||||
"""No polling needed for a demo fan."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
return self._speed
|
||||
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
@ -61,7 +67,12 @@ class DemoFan(FanEntity):
|
||||
|
||||
def set_speed(self, speed: str) -> None:
|
||||
"""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()
|
||||
|
||||
def oscillate(self, oscillating: bool) -> None:
|
||||
@ -69,6 +80,11 @@ class DemoFan(FanEntity):
|
||||
self.oscillating = oscillating
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def current_direction(self) -> str:
|
||||
"""Fan direction."""
|
||||
return self.direction
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
|
@ -64,7 +64,11 @@ class ISYFanDevice(isy.ISYDevice, FanEntity):
|
||||
def __init__(self, node) -> None:
|
||||
"""Initialize the ISY994 fan device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
self.speed = self.state
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
return self.state
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
|
92
homeassistant/components/fan/wink.py
Normal file
92
homeassistant/components/fan/wink.py
Normal 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
|
@ -8,6 +8,7 @@
|
||||
<link rel='icon' href='/static/icons/favicon.ico'>
|
||||
<link rel='apple-touch-icon' sizes='180x180'
|
||||
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() -%}
|
||||
<link rel='prefetch' href='{{ panel.url }}'>
|
||||
{% endfor -%}
|
||||
|
@ -1,18 +1,18 @@
|
||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "ad1ebcd0614c98a390d982087a7ca75c",
|
||||
"frontend.html": "826ee6a4b39c939e31aa468b1ef618f9",
|
||||
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
||||
"core.js": "22d39af274e1d824ca1302e10971f2d8",
|
||||
"frontend.html": "61e57194179b27563a05282b58dd4f47",
|
||||
"mdi.html": "5bb2f1717206bad0d187c2633062c575",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e",
|
||||
"panels/ha-panel-dev-info.html": "a9c07bf281fe9791fb15827ec1286825",
|
||||
"panels/ha-panel-dev-service.html": "ac74f7ce66fd7136d25c914ea12f4351",
|
||||
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
|
||||
"panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400",
|
||||
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
||||
"panels/ha-panel-dev-event.html": "f19840b9a6a46f57cb064b384e1353f5",
|
||||
"panels/ha-panel-dev-info.html": "3765a371478cc66d677cf6dcc35267c6",
|
||||
"panels/ha-panel-dev-service.html": "e32bcd3afdf485417a3e20b4fc760776",
|
||||
"panels/ha-panel-dev-state.html": "8257d99a38358a150eafdb23fa6727e0",
|
||||
"panels/ha-panel-dev-template.html": "cbb251acabd5e7431058ed507b70522b",
|
||||
"panels/ha-panel-history.html": "7baeb4bd7d9ce0def4f95eab6f10812e",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "4bc5c8370a85a4215413fbae8f85addb",
|
||||
"panels/ha-panel-logbook.html": "93de4cee3a2352a6813b5c218421d534",
|
||||
"panels/ha-panel-map.html": "3b0ca63286cbe80f27bd36dbc2434e89",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -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
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -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>
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user