forked from home-assistant/core
Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65970a2248 | |||
| 5d82f48c02 | |||
| 8e185bc300 | |||
| a013908115 | |||
| bdf6257640 | |||
| 1f50e335fa | |||
| 87f9f17335 | |||
| f101f6b7cb | |||
| abf07b60f0 | |||
| 0b114f0755 | |||
| 3ee8f58fdf | |||
| ff4da05267 | |||
| 17308a2730 | |||
| 7d9bce2153 | |||
| 2839f0ff5f | |||
| 2ec295a6f8 | |||
| 4bd7a7eee3 | |||
| d0cbbe6141 | |||
| 9efa31ef9f | |||
| 8a777f6e78 | |||
| ac13a2736b | |||
| 940577e105 | |||
| c917470836 | |||
| 47a344f3a1 | |||
| f744a29d9d | |||
| 3cd4cb741c | |||
| 1128104281 | |||
| d6d685a483 | |||
| 2c6e6c2a6f | |||
| c8e0de19b6 | |||
| b2440a6d95 | |||
| c36c3f0d64 | |||
| 0e7d284c83 | |||
| cdd111df49 | |||
| cccd0deb65 | |||
| e014a84215 | |||
| d549e26a9b | |||
| 08adfd87f7 | |||
| 65b0ec6615 | |||
| cb646e48d0 | |||
| fecce206a9 | |||
| 176ef411de | |||
| 2ac23c8be6 | |||
| a373793029 | |||
| 3153b0c8fc | |||
| 89d008d1f3 | |||
| 6755ae2605 | |||
| c18033ba85 | |||
| cdc5388dc9 | |||
| be4776d039 | |||
| 30111ea417 | |||
| 576c806e86 | |||
| 1c561eaf0d | |||
| 1da30032a0 | |||
| d5bbb6ffd2 | |||
| b4e5695bbd | |||
| 716ab0433f | |||
| 7d9ef97bda | |||
| 703b4354e0 | |||
| ce0ca7ff90 | |||
| 54e87836f6 | |||
| 5f4aa6d2ba | |||
| dc447a75c6 | |||
| ce7e9e36dd | |||
| 8aca2e84dc | |||
| f3e55ce330 | |||
| 20caeb5383 | |||
| bc0d0751b9 | |||
| 5393b073fe | |||
| d7b7370c82 | |||
| 5f65f67f1e | |||
| f242418986 | |||
| d3d9d9ebf2 | |||
| 8ceb57752b | |||
| bd1af8c3d8 | |||
| 6af995026b | |||
| b657cff6ba | |||
| e3fba79126 | |||
| bb0068908d | |||
| 0748466ffc | |||
| fe018fd58c | |||
| 10317a0f71 | |||
| 87d55834be | |||
| d4cc806cd5 | |||
| 1a7e8c88a3 | |||
| 90a51160c4 | |||
| 50321a29b5 | |||
| 67d137cfd5 | |||
| bb4d1773d3 | |||
| a6c1192bfc | |||
| d14d2fe588 | |||
| f696331563 | |||
| 6b2b92a732 | |||
| 83ce9450f7 | |||
| 0b405c33c4 | |||
| bf74cab7af | |||
| d8adb4bdb0 | |||
| bef15264b7 | |||
| 6d26915c69 | |||
| fa2e6ada26 | |||
| a6880c452f | |||
| 4bccb0d2a1 | |||
| 103639455c | |||
| 549abd9c7e | |||
| f1aba5511f | |||
| 21d05a8b4d | |||
| cb6c869c2f | |||
| 640e499964 | |||
| b3b4f7468d | |||
| ad9621ebe5 | |||
| e370d523ec | |||
| 61a41bb8fc | |||
| 2da6d3c223 | |||
| 816efa02d1 | |||
| bd1b1a9ff9 | |||
| 1d23f7f900 | |||
| 39843a73de | |||
| aec425d1f6 | |||
| 855ed2b4e4 | |||
| 29e659cf4c |
+23
-4
@@ -61,6 +61,9 @@ omit =
|
||||
homeassistant/components/coinbase.py
|
||||
homeassistant/components/sensor/coinbase.py
|
||||
|
||||
homeassistant/components/cast/*
|
||||
homeassistant/components/*/cast.py
|
||||
|
||||
homeassistant/components/comfoconnect.py
|
||||
homeassistant/components/*/comfoconnect.py
|
||||
|
||||
@@ -97,7 +100,7 @@ omit =
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/fritzbox.py
|
||||
homeassistant/components/*/fritzbox.py
|
||||
homeassistant/components/switch/fritzbox.py
|
||||
|
||||
homeassistant/components/eufy.py
|
||||
homeassistant/components/*/eufy.py
|
||||
@@ -195,12 +198,15 @@ omit =
|
||||
homeassistant/components/neato.py
|
||||
homeassistant/components/*/neato.py
|
||||
|
||||
homeassistant/components/nest.py
|
||||
homeassistant/components/nest/__init__.py
|
||||
homeassistant/components/*/nest.py
|
||||
|
||||
homeassistant/components/netatmo.py
|
||||
homeassistant/components/*/netatmo.py
|
||||
|
||||
homeassistant/components/netgear_lte.py
|
||||
homeassistant/components/*/netgear_lte.py
|
||||
|
||||
homeassistant/components/octoprint.py
|
||||
homeassistant/components/*/octoprint.py
|
||||
|
||||
@@ -249,6 +255,9 @@ omit =
|
||||
homeassistant/components/smappee.py
|
||||
homeassistant/components/*/smappee.py
|
||||
|
||||
homeassistant/components/sonos/__init__.py
|
||||
homeassistant/components/*/sonos.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
@@ -311,6 +320,9 @@ omit =
|
||||
homeassistant/components/wink/*
|
||||
homeassistant/components/*/wink.py
|
||||
|
||||
homeassistant/components/wirelesstag.py
|
||||
homeassistant/components/*/wirelesstag.py
|
||||
|
||||
homeassistant/components/xiaomi_aqara.py
|
||||
homeassistant/components/*/xiaomi_aqara.py
|
||||
|
||||
@@ -348,6 +360,7 @@ omit =
|
||||
homeassistant/components/binary_sensor/ping.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/binary_sensor/tapsaff.py
|
||||
homeassistant/components/binary_sensor/uptimerobot.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/calendar/caldav.py
|
||||
homeassistant/components/calendar/todoist.py
|
||||
@@ -363,6 +376,7 @@ omit =
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/synology.py
|
||||
homeassistant/components/camera/xeoma.py
|
||||
homeassistant/components/camera/xiaomi.py
|
||||
homeassistant/components/camera/yi.py
|
||||
homeassistant/components/climate/econet.py
|
||||
homeassistant/components/climate/ephember.py
|
||||
@@ -378,6 +392,7 @@ omit =
|
||||
homeassistant/components/climate/sensibo.py
|
||||
homeassistant/components/climate/touchline.py
|
||||
homeassistant/components/climate/venstar.py
|
||||
homeassistant/components/climate/zhong_hong.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/gogogate2.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
@@ -397,6 +412,7 @@ omit =
|
||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||
homeassistant/components/device_tracker/cisco_ios.py
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/freebox.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/google_maps.py
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
@@ -462,6 +478,7 @@ omit =
|
||||
homeassistant/components/light/yeelightsunflower.py
|
||||
homeassistant/components/light/zengge.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/lock/kiwi.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/lock/nello.py
|
||||
homeassistant/components/lock/nuki.py
|
||||
@@ -472,7 +489,6 @@ omit =
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
homeassistant/components/media_player/bluesound.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/channels.py
|
||||
homeassistant/components/media_player/clementine.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
@@ -481,10 +497,12 @@ omit =
|
||||
homeassistant/components/media_player/directv.py
|
||||
homeassistant/components/media_player/dunehd.py
|
||||
homeassistant/components/media_player/emby.py
|
||||
homeassistant/components/media_player/epson.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/frontier_silicon.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/gstreamer.py
|
||||
homeassistant/components/media_player/horizon.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
@@ -506,7 +524,6 @@ omit =
|
||||
homeassistant/components/media_player/russound_rnet.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/songpal.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/ue_smart_radio.py
|
||||
@@ -644,6 +661,7 @@ omit =
|
||||
homeassistant/components/sensor/nederlandse_spoorwegen.py
|
||||
homeassistant/components/sensor/netdata.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nsw_fuel_station.py
|
||||
homeassistant/components/sensor/nut.py
|
||||
homeassistant/components/sensor/nzbget.py
|
||||
homeassistant/components/sensor/ohmconnect.py
|
||||
@@ -748,6 +766,7 @@ omit =
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/vacuum/mqtt.py
|
||||
homeassistant/components/vacuum/roomba.py
|
||||
homeassistant/components/watson_iot.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/buienradar.py
|
||||
homeassistant/components/weather/darksky.py
|
||||
|
||||
@@ -70,6 +70,7 @@ homeassistant/components/sensor/filter.py @dgomes
|
||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/nsw_fuel_station.py @nickw444
|
||||
homeassistant/components/sensor/pollen.py @bachya
|
||||
homeassistant/components/sensor/qnap.py @colinodell
|
||||
homeassistant/components/sensor/sma.py @kellerza
|
||||
|
||||
+10
-19
@@ -1,5 +1,4 @@
|
||||
"""Provide methods to bootstrap a Home Assistant instance."""
|
||||
import asyncio
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
@@ -17,7 +16,7 @@ from homeassistant.components import persistent_notification
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.package import async_get_user_site, get_user_site
|
||||
from homeassistant.util.package import async_get_user_site, is_virtual_env
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.signal import async_register_signal_handling
|
||||
@@ -53,8 +52,9 @@ def from_config_dict(config: Dict[str, Any],
|
||||
if config_dir is not None:
|
||||
config_dir = os.path.abspath(config_dir)
|
||||
hass.config.config_dir = config_dir
|
||||
hass.loop.run_until_complete(
|
||||
async_mount_local_lib_path(config_dir, hass.loop))
|
||||
if not is_virtual_env():
|
||||
hass.loop.run_until_complete(
|
||||
async_mount_local_lib_path(config_dir))
|
||||
|
||||
# run task
|
||||
hass = hass.loop.run_until_complete(
|
||||
@@ -197,7 +197,9 @@ async def async_from_config_file(config_path: str,
|
||||
# Set config dir to directory holding config file
|
||||
config_dir = os.path.abspath(os.path.dirname(config_path))
|
||||
hass.config.config_dir = config_dir
|
||||
await async_mount_local_lib_path(config_dir, hass.loop)
|
||||
|
||||
if not is_virtual_env():
|
||||
await async_mount_local_lib_path(config_dir)
|
||||
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
||||
log_no_color)
|
||||
@@ -211,9 +213,8 @@ async def async_from_config_file(config_path: str,
|
||||
finally:
|
||||
clear_secret_cache()
|
||||
|
||||
hass = await async_from_config_dict(
|
||||
return await async_from_config_dict(
|
||||
config_dict, hass, enable_log=False, skip_pip=skip_pip)
|
||||
return hass
|
||||
|
||||
|
||||
@core.callback
|
||||
@@ -308,23 +309,13 @@ def async_enable_logging(hass: core.HomeAssistant,
|
||||
"Unable to setup error log %s (access denied)", err_log_path)
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path."""
|
||||
deps_dir = os.path.join(config_dir, 'deps')
|
||||
lib_dir = get_user_site(deps_dir)
|
||||
if lib_dir not in sys.path:
|
||||
sys.path.insert(0, lib_dir)
|
||||
return deps_dir
|
||||
|
||||
|
||||
async def async_mount_local_lib_path(config_dir: str,
|
||||
loop: asyncio.AbstractEventLoop) -> str:
|
||||
async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path.
|
||||
|
||||
This function is a coroutine.
|
||||
"""
|
||||
deps_dir = os.path.join(config_dir, 'deps')
|
||||
lib_dir = await async_get_user_site(deps_dir, loop=loop)
|
||||
lib_dir = await async_get_user_site(deps_dir)
|
||||
if lib_dir not in sys.path:
|
||||
sys.path.insert(0, lib_dir)
|
||||
return deps_dir
|
||||
|
||||
@@ -4,15 +4,17 @@ Support for Arlo Alarm Control Panels.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.arlo/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanel, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.arlo import (DATA_ARLO, CONF_ATTRIBUTION)
|
||||
from homeassistant.components.arlo import (
|
||||
DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
@@ -36,21 +38,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Arlo Alarm Control Panels."""
|
||||
data = hass.data[DATA_ARLO]
|
||||
arlo = hass.data[DATA_ARLO]
|
||||
|
||||
if not data.base_stations:
|
||||
if not arlo.base_stations:
|
||||
return
|
||||
|
||||
home_mode_name = config.get(CONF_HOME_MODE_NAME)
|
||||
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
|
||||
base_stations = []
|
||||
for base_station in data.base_stations:
|
||||
for base_station in arlo.base_stations:
|
||||
base_stations.append(ArloBaseStation(base_station, home_mode_name,
|
||||
away_mode_name))
|
||||
async_add_devices(base_stations, True)
|
||||
add_devices(base_stations, True)
|
||||
|
||||
|
||||
class ArloBaseStation(AlarmControlPanel):
|
||||
@@ -68,6 +69,16 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
"""Return icon."""
|
||||
return ICON
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
@@ -75,30 +86,22 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the device."""
|
||||
# PyArlo sometimes returns None for mode. So retry 3 times before
|
||||
# returning None.
|
||||
num_retries = 3
|
||||
i = 0
|
||||
while i < num_retries:
|
||||
mode = self._base_station.mode
|
||||
if mode:
|
||||
self._state = self._get_state_from_mode(mode)
|
||||
return
|
||||
i += 1
|
||||
self._state = None
|
||||
_LOGGER.debug("Updating Arlo Alarm Control Panel %s", self.name)
|
||||
mode = self._base_station.mode
|
||||
if mode:
|
||||
self._state = self._get_state_from_mode(mode)
|
||||
else:
|
||||
self._state = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._base_station.mode = DISARMED
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command. Uses custom mode."""
|
||||
self._base_station.mode = self._away_mode_name
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command. Uses custom mode."""
|
||||
self._base_station.mode = self._home_mode_name
|
||||
|
||||
@@ -125,4 +128,4 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif mode == self._away_mode_name:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return None
|
||||
return mode
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.2.2']
|
||||
REQUIREMENTS = ['amcrest==1.2.3']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -5,14 +5,18 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/arlo/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL)
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.1.2']
|
||||
REQUIREMENTS = ['pyarlo==0.1.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,10 +29,16 @@ DOMAIN = 'arlo'
|
||||
NOTIFICATION_ID = 'arlo_notification'
|
||||
NOTIFICATION_TITLE = 'Arlo Component Setup'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
SIGNAL_UPDATE_ARLO = "arlo_update"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -38,6 +48,7 @@ def setup(hass, config):
|
||||
conf = config[DOMAIN]
|
||||
username = conf.get(CONF_USERNAME)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
scan_interval = conf.get(CONF_SCAN_INTERVAL)
|
||||
|
||||
try:
|
||||
from pyarlo import PyArlo
|
||||
@@ -45,7 +56,17 @@ def setup(hass, config):
|
||||
arlo = PyArlo(username, password, preload=False)
|
||||
if not arlo.is_connected:
|
||||
return False
|
||||
|
||||
# assign refresh period to base station thread
|
||||
arlo_base_station = next((
|
||||
station for station in arlo.base_stations), None)
|
||||
|
||||
if arlo_base_station is None:
|
||||
return False
|
||||
|
||||
arlo_base_station.refresh_rate = scan_interval.total_seconds()
|
||||
hass.data[DATA_ARLO] = arlo
|
||||
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex))
|
||||
hass.components.persistent_notification.create(
|
||||
@@ -55,4 +76,17 @@ def setup(hass, config):
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
def hub_refresh(event_time):
|
||||
"""Call ArloHub to refresh information."""
|
||||
_LOGGER.info("Updating Arlo Hub component")
|
||||
hass.data[DATA_ARLO].update(update_cameras=True,
|
||||
update_base_station=True)
|
||||
dispatcher_send(hass, SIGNAL_UPDATE_ARLO)
|
||||
|
||||
# register service
|
||||
hass.services.register(DOMAIN, 'update', hub_refresh)
|
||||
|
||||
# register scan interval for ArloHub
|
||||
track_time_interval(hass, hub_refresh, scan_interval)
|
||||
return True
|
||||
|
||||
@@ -145,7 +145,7 @@ def request_configuration(hass, config, name, host, serialnumber):
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up for Axis devices."""
|
||||
def _shutdown(call): # pylint: disable=unused-argument
|
||||
def _shutdown(call):
|
||||
"""Stop the event stream on shutdown."""
|
||||
for serialnumber, device in AXIS_DEVICES.items():
|
||||
_LOGGER.info("Stopping event stream for %s.", serialnumber)
|
||||
@@ -272,8 +272,7 @@ class AxisDeviceEvent(Entity):
|
||||
|
||||
def _update_callback(self):
|
||||
"""Update the sensor's state, if needed."""
|
||||
self.update()
|
||||
self.schedule_update_ha_state()
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -35,7 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Command line Binary Sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.eight_sleep/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.eight_sleep import (
|
||||
@@ -16,8 +15,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['eight_sleep']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the eight sleep binary sensor."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
@@ -63,7 +62,6 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._state = self._usrobj.bed_presence
|
||||
|
||||
@@ -23,7 +23,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the GC100 devices."""
|
||||
binary_sensors = []
|
||||
|
||||
@@ -28,7 +28,6 @@ ISY_DEVICE_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 binary sensor platform."""
|
||||
@@ -299,7 +298,6 @@ class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice):
|
||||
# No heartbeat timer is active
|
||||
pass
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@callback
|
||||
def timer_elapsed(now) -> None:
|
||||
"""Heartbeat missed; set state to indicate dead battery."""
|
||||
@@ -314,7 +312,6 @@ class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice):
|
||||
self._heartbeat_timer = async_track_point_in_utc_time(
|
||||
self.hass, timer_elapsed, point_in_time)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def on_update(self, event: object) -> None:
|
||||
"""Ignore node status updates.
|
||||
|
||||
|
||||
@@ -115,7 +115,6 @@ class KNXBinarySensor(BinarySensorDevice):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
await self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ https://home-assistant.io/components/binary_sensor.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -24,7 +25,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'MQTT Binary sensor'
|
||||
|
||||
CONF_UNIQUE_ID = 'unique_id'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_FORCE_UPDATE = False
|
||||
@@ -37,6 +38,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
|
||||
# Integrations shouldn't never expose unique_id through configuration
|
||||
# this here is an exception because MQTT is a msg transport, not a protocol
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
@@ -61,7 +65,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
config.get(CONF_PAYLOAD_OFF),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
value_template
|
||||
value_template,
|
||||
config.get(CONF_UNIQUE_ID),
|
||||
)])
|
||||
|
||||
|
||||
@@ -70,7 +75,8 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
||||
|
||||
def __init__(self, name, state_topic, availability_topic, device_class,
|
||||
qos, force_update, payload_on, payload_off, payload_available,
|
||||
payload_not_available, value_template):
|
||||
payload_not_available, value_template,
|
||||
unique_id: Optional[str]):
|
||||
"""Initialize the MQTT binary sensor."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
payload_not_available)
|
||||
@@ -83,6 +89,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
||||
self._qos = qos
|
||||
self._force_update = force_update
|
||||
self._template = value_template
|
||||
self._unique_id = unique_id
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
@@ -134,3 +141,8 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
||||
def force_update(self):
|
||||
"""Force update."""
|
||||
return self._force_update
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@@ -29,6 +29,7 @@ class MyStromView(HomeAssistantView):
|
||||
|
||||
url = '/api/mystrom'
|
||||
name = 'api:mystrom'
|
||||
supported_actions = ['single', 'double', 'long', 'touch']
|
||||
|
||||
def __init__(self, add_devices):
|
||||
"""Initialize the myStrom URL endpoint."""
|
||||
@@ -44,16 +45,18 @@ class MyStromView(HomeAssistantView):
|
||||
@asyncio.coroutine
|
||||
def _handle(self, hass, data):
|
||||
"""Handle requests to the myStrom endpoint."""
|
||||
button_action = list(data.keys())[0]
|
||||
button_id = data[button_action]
|
||||
entity_id = '{}.{}_{}'.format(DOMAIN, button_id, button_action)
|
||||
button_action = next((
|
||||
parameter for parameter in data
|
||||
if parameter in self.supported_actions), None)
|
||||
|
||||
if button_action not in ['single', 'double', 'long', 'touch']:
|
||||
if button_action is None:
|
||||
_LOGGER.error(
|
||||
"Received unidentified message from myStrom button: %s", data)
|
||||
return ("Received unidentified message: {}".format(data),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
button_id = data[button_action]
|
||||
entity_id = '{}.{}_{}'.format(DOMAIN, button_id, button_action)
|
||||
if entity_id not in self.buttons:
|
||||
_LOGGER.info("New myStrom button/action detected: %s/%s",
|
||||
button_id, button_action)
|
||||
|
||||
@@ -8,7 +8,8 @@ from itertools import chain
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.nest import DATA_NEST, NestSensorDevice
|
||||
from homeassistant.components.nest import (
|
||||
DATA_NEST, DATA_NEST_CONFIG, CONF_BINARY_SENSORS, NestSensorDevice)
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
@@ -56,12 +57,19 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Nest binary sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
"""Set up the Nest binary sensors.
|
||||
|
||||
No longer used.
|
||||
"""
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_devices):
|
||||
"""Set up a Nest binary sensor based on a config entry."""
|
||||
nest = hass.data[DATA_NEST]
|
||||
|
||||
discovery_info = \
|
||||
hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_BINARY_SENSORS, {})
|
||||
|
||||
# Add all available binary sensors if no Nest binary sensor config is set
|
||||
if discovery_info == {}:
|
||||
conditions = _VALID_BINARY_SENSOR_TYPES
|
||||
@@ -76,32 +84,37 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"for valid options.")
|
||||
_LOGGER.error(wstr)
|
||||
|
||||
sensors = []
|
||||
for structure in nest.structures():
|
||||
sensors += [NestBinarySensor(structure, None, variable)
|
||||
for variable in conditions
|
||||
if variable in STRUCTURE_BINARY_TYPES]
|
||||
device_chain = chain(nest.thermostats(),
|
||||
nest.smoke_co_alarms(),
|
||||
nest.cameras())
|
||||
for structure, device in device_chain:
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conditions
|
||||
if variable in BINARY_TYPES]
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conditions
|
||||
if variable in CLIMATE_BINARY_TYPES
|
||||
and device.is_thermostat]
|
||||
|
||||
if device.is_camera:
|
||||
def get_binary_sensors():
|
||||
"""Get the Nest binary sensors."""
|
||||
sensors = []
|
||||
for structure in nest.structures():
|
||||
sensors += [NestBinarySensor(structure, None, variable)
|
||||
for variable in conditions
|
||||
if variable in STRUCTURE_BINARY_TYPES]
|
||||
device_chain = chain(nest.thermostats(),
|
||||
nest.smoke_co_alarms(),
|
||||
nest.cameras())
|
||||
for structure, device in device_chain:
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conditions
|
||||
if variable in CAMERA_BINARY_TYPES]
|
||||
for activity_zone in device.activity_zones:
|
||||
sensors += [NestActivityZoneSensor(structure,
|
||||
device,
|
||||
activity_zone)]
|
||||
add_devices(sensors, True)
|
||||
if variable in BINARY_TYPES]
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conditions
|
||||
if variable in CLIMATE_BINARY_TYPES
|
||||
and device.is_thermostat]
|
||||
|
||||
if device.is_camera:
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conditions
|
||||
if variable in CAMERA_BINARY_TYPES]
|
||||
for activity_zone in device.activity_zones:
|
||||
sensors += [NestActivityZoneSensor(structure,
|
||||
device,
|
||||
activity_zone)]
|
||||
|
||||
return sensors
|
||||
|
||||
async_add_devices(await hass.async_add_job(get_binary_sensors), True)
|
||||
|
||||
|
||||
class NestBinarySensor(NestSensorDevice, BinarySensorDevice):
|
||||
|
||||
@@ -57,7 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the access to Netatmo binary sensor."""
|
||||
netatmo = hass.components.netatmo
|
||||
@@ -68,12 +67,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
module_name = None
|
||||
|
||||
import lnetatmo
|
||||
import pyatmo
|
||||
try:
|
||||
data = CameraData(netatmo.NETATMO_AUTH, home)
|
||||
if not data.get_camera_names():
|
||||
return None
|
||||
except lnetatmo.NoDevice:
|
||||
except pyatmo.NoDevice:
|
||||
return None
|
||||
|
||||
welcome_sensors = config.get(
|
||||
|
||||
@@ -33,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the available OctoPrint binary sensors."""
|
||||
octoprint_api = hass.data[DOMAIN]["api"]
|
||||
|
||||
@@ -44,7 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Pilight Binary Sensor."""
|
||||
disarm = config.get(CONF_DISARM_AFTER_TRIGGER)
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.rainmachine import (
|
||||
BINARY_SENSORS, DATA_RAINMACHINE, DATA_UPDATE_TOPIC, TYPE_FREEZE,
|
||||
BINARY_SENSORS, DATA_RAINMACHINE, SENSOR_UPDATE_TOPIC, TYPE_FREEZE,
|
||||
TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH,
|
||||
TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity)
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
@@ -20,7 +20,8 @@ DEPENDENCIES = ['rainmachine']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the RainMachine Switch platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
@@ -33,7 +34,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
binary_sensors.append(
|
||||
RainMachineBinarySensor(rainmachine, sensor_type, name, icon))
|
||||
|
||||
add_devices(binary_sensors, True)
|
||||
async_add_devices(binary_sensors, True)
|
||||
|
||||
|
||||
class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
|
||||
@@ -70,16 +71,16 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
|
||||
self.rainmachine.device_mac.replace(':', ''), self._sensor_type)
|
||||
|
||||
@callback
|
||||
def update_data(self):
|
||||
def _update_data(self):
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC,
|
||||
self.update_data)
|
||||
async_dispatcher_connect(
|
||||
self.hass, SENSOR_UPDATE_TOPIC, self._update_data)
|
||||
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Update the state."""
|
||||
if self._sensor_type == TYPE_FREEZE:
|
||||
self._state = self.rainmachine.restrictions['current']['freeze']
|
||||
|
||||
@@ -42,7 +42,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the raspihats binary_sensor devices."""
|
||||
I2CHatBinarySensor.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER]
|
||||
|
||||
@@ -39,7 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Raspberry PI GPIO devices."""
|
||||
pull_mode = config.get(CONF_PULL_MODE)
|
||||
|
||||
@@ -94,4 +94,4 @@ class SkybellBinarySensor(SkybellDevice, BinarySensorDevice):
|
||||
|
||||
self._state = bool(event and event.get('id') != self._event.get('id'))
|
||||
|
||||
self._event = event
|
||||
self._event = event or {}
|
||||
|
||||
@@ -57,7 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the trend sensors."""
|
||||
sensors = []
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
A platform that to monitor Uptime Robot monitors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://www.home-assistant.io/components/binary_sensor.uptimerobot/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyuptimerobot==0.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_TARGET = 'target'
|
||||
|
||||
CONF_ATTRIBUTION = "Data provided by Uptime Robot"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Uptime Robot binary_sensors."""
|
||||
from pyuptimerobot import UptimeRobot
|
||||
|
||||
up_robot = UptimeRobot()
|
||||
api_key = config.get(CONF_API_KEY)
|
||||
monitors = up_robot.getMonitors(api_key)
|
||||
|
||||
devices = []
|
||||
if not monitors or monitors.get('stat') != 'ok':
|
||||
_LOGGER.error("Error connecting to Uptime Robot")
|
||||
return
|
||||
|
||||
for monitor in monitors['monitors']:
|
||||
devices.append(UptimeRobotBinarySensor(
|
||||
api_key, up_robot, monitor['id'], monitor['friendly_name'],
|
||||
monitor['url']))
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class UptimeRobotBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Uptime Robot binary sensor."""
|
||||
|
||||
def __init__(self, api_key, up_robot, monitor_id, name, target):
|
||||
"""Initialize Uptime Robot the binary sensor."""
|
||||
self._api_key = api_key
|
||||
self._monitor_id = str(monitor_id)
|
||||
self._name = name
|
||||
self._target = target
|
||||
self._up_robot = up_robot
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'connectivity'
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the binary sensor."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
ATTR_TARGET: self._target,
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state of the binary sensor."""
|
||||
monitor = self._up_robot.getMonitors(self._api_key, self._monitor_id)
|
||||
if not monitor or monitor.get('stat') != 'ok':
|
||||
_LOGGER.warning("Failed to get new state")
|
||||
return
|
||||
status = monitor['monitors'][0]['status']
|
||||
self._state = 1 if status == 2 else 0
|
||||
@@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Perform the setup for Vera controller devices."""
|
||||
add_devices(
|
||||
VeraBinarySensor(device, hass.data[VERA_CONTROLLER])
|
||||
for device in hass.data[VERA_DEVICES]['binary_sensor'])
|
||||
[VeraBinarySensor(device, hass.data[VERA_CONTROLLER])
|
||||
for device in hass.data[VERA_DEVICES]['binary_sensor']], True)
|
||||
|
||||
|
||||
class VeraBinarySensor(VeraDevice, BinarySensorDevice):
|
||||
|
||||
@@ -54,6 +54,7 @@ class VerisureDoorWindowSensor(BinarySensorDevice):
|
||||
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]",
|
||||
self._device_label) is not None
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def update(self):
|
||||
"""Update the state of the sensor."""
|
||||
hub.update_overview()
|
||||
|
||||
@@ -13,7 +13,7 @@ DEPENDENCIES = ['wemo']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument, too-many-function-args
|
||||
# pylint: disable=too-many-function-args
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Register discovered WeMo binary sensors."""
|
||||
import pywemo.discovery as discovery
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
"""
|
||||
Binary sensor support for Wireless Sensor Tags.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.wirelesstag/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.wirelesstag import (
|
||||
DOMAIN as WIRELESSTAG_DOMAIN,
|
||||
WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER,
|
||||
WIRELESSTAG_TYPE_ALSPRO,
|
||||
WIRELESSTAG_TYPE_WEMO_DEVICE,
|
||||
SIGNAL_BINARY_EVENT_UPDATE,
|
||||
WirelessTagBaseSensor)
|
||||
from homeassistant.const import (
|
||||
CONF_MONITORED_CONDITIONS, STATE_ON, STATE_OFF)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['wirelesstag']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# On means in range, Off means out of range
|
||||
SENSOR_PRESENCE = 'presence'
|
||||
|
||||
# On means motion detected, Off means cear
|
||||
SENSOR_MOTION = 'motion'
|
||||
|
||||
# On means open, Off means closed
|
||||
SENSOR_DOOR = 'door'
|
||||
|
||||
# On means temperature become too cold, Off means normal
|
||||
SENSOR_COLD = 'cold'
|
||||
|
||||
# On means hot, Off means normal
|
||||
SENSOR_HEAT = 'heat'
|
||||
|
||||
# On means too dry (humidity), Off means normal
|
||||
SENSOR_DRY = 'dry'
|
||||
|
||||
# On means too wet (humidity), Off means normal
|
||||
SENSOR_WET = 'wet'
|
||||
|
||||
# On means light detected, Off means no light
|
||||
SENSOR_LIGHT = 'light'
|
||||
|
||||
# On means moisture detected (wet), Off means no moisture (dry)
|
||||
SENSOR_MOISTURE = 'moisture'
|
||||
|
||||
# On means tag battery is low, Off means normal
|
||||
SENSOR_BATTERY = 'low_battery'
|
||||
|
||||
# Sensor types: Name, device_class, push notification type representing 'on',
|
||||
# attr to check
|
||||
SENSOR_TYPES = {
|
||||
SENSOR_PRESENCE: ['Presence', 'presence', 'is_in_range', {
|
||||
"on": "oor",
|
||||
"off": "back_in_range"
|
||||
}, 2],
|
||||
SENSOR_MOTION: ['Motion', 'motion', 'is_moved', {
|
||||
"on": "motion_detected",
|
||||
}, 5],
|
||||
SENSOR_DOOR: ['Door', 'door', 'is_door_open', {
|
||||
"on": "door_opened",
|
||||
"off": "door_closed"
|
||||
}, 5],
|
||||
SENSOR_COLD: ['Cold', 'cold', 'is_cold', {
|
||||
"on": "temp_toolow",
|
||||
"off": "temp_normal"
|
||||
}, 4],
|
||||
SENSOR_HEAT: ['Heat', 'heat', 'is_heat', {
|
||||
"on": "temp_toohigh",
|
||||
"off": "temp_normal"
|
||||
}, 4],
|
||||
SENSOR_DRY: ['Too dry', 'dry', 'is_too_dry', {
|
||||
"on": "too_dry",
|
||||
"off": "cap_normal"
|
||||
}, 2],
|
||||
SENSOR_WET: ['Too wet', 'wet', 'is_too_humid', {
|
||||
"on": "too_humid",
|
||||
"off": "cap_normal"
|
||||
}, 2],
|
||||
SENSOR_LIGHT: ['Light', 'light', 'is_light_on', {
|
||||
"on": "too_bright",
|
||||
"off": "light_normal"
|
||||
}, 1],
|
||||
SENSOR_MOISTURE: ['Leak', 'moisture', 'is_leaking', {
|
||||
"on": "water_detected",
|
||||
"off": "water_dried",
|
||||
}, 1],
|
||||
SENSOR_BATTERY: ['Low Battery', 'battery', 'is_battery_low', {
|
||||
"on": "low_battery"
|
||||
}, 3]
|
||||
}
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the platform for a WirelessTags."""
|
||||
platform = hass.data.get(WIRELESSTAG_DOMAIN)
|
||||
|
||||
sensors = []
|
||||
tags = platform.tags
|
||||
for tag in tags.values():
|
||||
allowed_sensor_types = WirelessTagBinarySensor.allowed_sensors(tag)
|
||||
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
|
||||
if sensor_type in allowed_sensor_types:
|
||||
sensors.append(WirelessTagBinarySensor(platform, tag,
|
||||
sensor_type))
|
||||
|
||||
add_devices(sensors, True)
|
||||
hass.add_job(platform.install_push_notifications, sensors)
|
||||
|
||||
|
||||
class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
|
||||
"""A binary sensor implementation for WirelessTags."""
|
||||
|
||||
@classmethod
|
||||
def allowed_sensors(cls, tag):
|
||||
"""Return list of allowed sensor types for specific tag type."""
|
||||
sensors_map = {
|
||||
# 13-bit tag - allows everything but not light and moisture
|
||||
WIRELESSTAG_TYPE_13BIT: [
|
||||
SENSOR_PRESENCE, SENSOR_BATTERY,
|
||||
SENSOR_MOTION, SENSOR_DOOR,
|
||||
SENSOR_COLD, SENSOR_HEAT,
|
||||
SENSOR_DRY, SENSOR_WET],
|
||||
|
||||
# Moister/water sensor - temperature and moisture only
|
||||
WIRELESSTAG_TYPE_WATER: [
|
||||
SENSOR_PRESENCE, SENSOR_BATTERY,
|
||||
SENSOR_COLD, SENSOR_HEAT,
|
||||
SENSOR_MOISTURE],
|
||||
|
||||
# ALS Pro: allows everything, but not moisture
|
||||
WIRELESSTAG_TYPE_ALSPRO: [
|
||||
SENSOR_PRESENCE, SENSOR_BATTERY,
|
||||
SENSOR_MOTION, SENSOR_DOOR,
|
||||
SENSOR_COLD, SENSOR_HEAT,
|
||||
SENSOR_DRY, SENSOR_WET,
|
||||
SENSOR_LIGHT],
|
||||
|
||||
# Wemo are power switches.
|
||||
WIRELESSTAG_TYPE_WEMO_DEVICE: [SENSOR_PRESENCE]
|
||||
}
|
||||
|
||||
# allow everything if tag type is unknown
|
||||
# (i just dont have full catalog of them :))
|
||||
tag_type = tag.tag_type
|
||||
fullset = SENSOR_TYPES.keys()
|
||||
return sensors_map[tag_type] if tag_type in sensors_map else fullset
|
||||
|
||||
def __init__(self, api, tag, sensor_type):
|
||||
"""Initialize a binary sensor for a Wireless Sensor Tags."""
|
||||
super().__init__(api, tag)
|
||||
self._sensor_type = sensor_type
|
||||
self._name = '{0} {1}'.format(self._tag.name,
|
||||
SENSOR_TYPES[self._sensor_type][0])
|
||||
self._device_class = SENSOR_TYPES[self._sensor_type][1]
|
||||
self._tag_attr = SENSOR_TYPES[self._sensor_type][2]
|
||||
self.binary_spec = SENSOR_TYPES[self._sensor_type][3]
|
||||
self.tag_id_index_template = SENSOR_TYPES[self._sensor_type][4]
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
tag_id = self.tag_id
|
||||
event_type = self.device_class
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type),
|
||||
self._on_binary_event_callback)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self._state == STATE_ON
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the binary sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def principal_value(self):
|
||||
"""Return value of tag.
|
||||
|
||||
Subclasses need override based on type of sensor.
|
||||
"""
|
||||
return (
|
||||
STATE_ON if getattr(self._tag, self._tag_attr, False)
|
||||
else STATE_OFF)
|
||||
|
||||
def updated_state_value(self):
|
||||
"""Use raw princial value."""
|
||||
return self.principal_value
|
||||
|
||||
@callback
|
||||
def _on_binary_event_callback(self, event):
|
||||
"""Update state from arrive push notification."""
|
||||
# state should be 'on' or 'off'
|
||||
self._state = event.data.get('state')
|
||||
self.async_schedule_update_ha_state()
|
||||
@@ -28,7 +28,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if model in ['motion', 'sensor_motion', 'sensor_motion.aq2']:
|
||||
devices.append(XiaomiMotionSensor(device, hass, gateway))
|
||||
elif model in ['magnet', 'sensor_magnet', 'sensor_magnet.aq2']:
|
||||
devices.append(XiaomiDoorSensor(device, gateway))
|
||||
if 'proto' not in device or int(device['proto'][0:1]) == 1:
|
||||
data_key = 'status'
|
||||
else:
|
||||
data_key = 'window_status'
|
||||
devices.append(XiaomiDoorSensor(device, data_key, gateway))
|
||||
elif model == 'sensor_wleak.aq1':
|
||||
devices.append(XiaomiWaterLeakSensor(device, gateway))
|
||||
elif model in ['smoke', 'sensor_smoke']:
|
||||
@@ -43,10 +47,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
data_key = 'channel_0'
|
||||
devices.append(XiaomiButton(device, 'Switch', data_key,
|
||||
hass, gateway))
|
||||
elif model in ['86sw1', 'sensor_86sw1.aq1']:
|
||||
elif model in ['86sw1', 'sensor_86sw1', 'sensor_86sw1.aq1']:
|
||||
devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0',
|
||||
hass, gateway))
|
||||
elif model in ['86sw2', 'sensor_86sw2.aq1']:
|
||||
elif model in ['86sw2', 'sensor_86sw2', 'sensor_86sw2.aq1']:
|
||||
devices.append(XiaomiButton(device, 'Wall Switch (Left)',
|
||||
'channel_0', hass, gateway))
|
||||
devices.append(XiaomiButton(device, 'Wall Switch (Right)',
|
||||
@@ -190,11 +194,11 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||
class XiaomiDoorSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiDoorSensor."""
|
||||
|
||||
def __init__(self, device, xiaomi_hub):
|
||||
def __init__(self, device, data_key, xiaomi_hub):
|
||||
"""Initialize the XiaomiDoorSensor."""
|
||||
self._open_since = 0
|
||||
XiaomiBinarySensor.__init__(self, device, 'Door Window Sensor',
|
||||
xiaomi_hub, 'status', 'opening')
|
||||
xiaomi_hub, data_key, 'opening')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
@@ -330,7 +334,7 @@ class XiaomiButton(XiaomiBinarySensor):
|
||||
click_type = 'both'
|
||||
elif value == 'shake':
|
||||
click_type = 'shake'
|
||||
elif value == 'long_click':
|
||||
elif value in ['long_click', 'long_both_click']:
|
||||
return False
|
||||
else:
|
||||
_LOGGER.warning("Unsupported click_type detected: %s", value)
|
||||
|
||||
@@ -187,8 +187,8 @@ class Switch(zha.Entity, BinarySensorDevice):
|
||||
if args[0] == 0xff:
|
||||
rate = 10 # Should read default move rate
|
||||
self._entity.move_level(-rate if args[0] else rate)
|
||||
elif command_id == 0x0002: # step
|
||||
# Step (technically shouldn't change on/off)
|
||||
elif command_id in (0x0002, 0x0006): # step, -with_on_off
|
||||
# Step (technically may change on/off)
|
||||
self._entity.move_level(-args[1] if args[0] else args[1])
|
||||
|
||||
def attribute_update(self, attrid, value):
|
||||
|
||||
@@ -34,7 +34,6 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
"""Set up the BloomSky component."""
|
||||
api_key = config[DOMAIN][CONF_API_KEY]
|
||||
|
||||
@@ -4,11 +4,12 @@ Support for Google Calendar event device sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/calendar/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import re
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from homeassistant.components.google import (
|
||||
CONF_OFFSET, CONF_DEVICE_ID, CONF_NAME)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
@@ -18,23 +19,32 @@ from homeassistant.helpers.entity import Entity, generate_entity_id
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.template import DATE_STR_FORMAT
|
||||
from homeassistant.util import dt
|
||||
from homeassistant.components import http
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'calendar'
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Track states and offer events for calendars."""
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DOMAIN)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
hass.http.register_view(CalendarListView(component))
|
||||
hass.http.register_view(CalendarEventView(component))
|
||||
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
'calendar', 'calendar', 'hass:calendar')
|
||||
|
||||
await component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
@@ -42,7 +52,14 @@ DEFAULT_CONF_TRACK_NEW = True
|
||||
DEFAULT_CONF_OFFSET = '!!'
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def get_date(date):
|
||||
"""Get the dateTime from date or dateTime as a local."""
|
||||
if 'date' in date:
|
||||
return dt.start_of_local_day(dt.dt.datetime.combine(
|
||||
dt.parse_date(date['date']), dt.dt.time.min))
|
||||
return dt.as_local(dt.parse_datetime(date['dateTime']))
|
||||
|
||||
|
||||
class CalendarEventDevice(Entity):
|
||||
"""A calendar event device."""
|
||||
|
||||
@@ -50,7 +67,6 @@ class CalendarEventDevice(Entity):
|
||||
# with an update() method
|
||||
data = None
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, data):
|
||||
"""Create the Calendar Event Device."""
|
||||
self._name = data.get(CONF_NAME)
|
||||
@@ -144,15 +160,8 @@ class CalendarEventDevice(Entity):
|
||||
self.cleanup()
|
||||
return
|
||||
|
||||
def _get_date(date):
|
||||
"""Get the dateTime from date or dateTime as a local."""
|
||||
if 'date' in date:
|
||||
return dt.start_of_local_day(dt.dt.datetime.combine(
|
||||
dt.parse_date(date['date']), dt.dt.time.min))
|
||||
return dt.as_local(dt.parse_datetime(date['dateTime']))
|
||||
|
||||
start = _get_date(self.data.event['start'])
|
||||
end = _get_date(self.data.event['end'])
|
||||
start = get_date(self.data.event['start'])
|
||||
end = get_date(self.data.event['end'])
|
||||
|
||||
summary = self.data.event.get('summary', '')
|
||||
|
||||
@@ -176,10 +185,61 @@ class CalendarEventDevice(Entity):
|
||||
|
||||
# cleanup the string so we don't have a bunch of double+ spaces
|
||||
self._cal_data['message'] = re.sub(' +', '', summary).strip()
|
||||
|
||||
self._cal_data['offset_time'] = offset_time
|
||||
self._cal_data['location'] = self.data.event.get('location', '')
|
||||
self._cal_data['description'] = self.data.event.get('description', '')
|
||||
self._cal_data['start'] = start
|
||||
self._cal_data['end'] = end
|
||||
self._cal_data['all_day'] = 'date' in self.data.event['start']
|
||||
|
||||
|
||||
class CalendarEventView(http.HomeAssistantView):
|
||||
"""View to retrieve calendar content."""
|
||||
|
||||
url = '/api/calendars/{entity_id}'
|
||||
name = 'api:calendars:calendar'
|
||||
|
||||
def __init__(self, component):
|
||||
"""Initialize calendar view."""
|
||||
self.component = component
|
||||
|
||||
async def get(self, request, entity_id):
|
||||
"""Return calendar events."""
|
||||
entity = self.component.get_entity(entity_id)
|
||||
start = request.query.get('start')
|
||||
end = request.query.get('end')
|
||||
if None in (start, end, entity):
|
||||
return web.Response(status=400)
|
||||
try:
|
||||
start_date = dt.parse_datetime(start)
|
||||
end_date = dt.parse_datetime(end)
|
||||
except (ValueError, AttributeError):
|
||||
return web.Response(status=400)
|
||||
event_list = await entity.async_get_events(
|
||||
request.app['hass'], start_date, end_date)
|
||||
return self.json(event_list)
|
||||
|
||||
|
||||
class CalendarListView(http.HomeAssistantView):
|
||||
"""View to retrieve calendar list."""
|
||||
|
||||
url = '/api/calendars'
|
||||
name = "api:calendars"
|
||||
|
||||
def __init__(self, component):
|
||||
"""Initialize calendar view."""
|
||||
self.component = component
|
||||
|
||||
async def get(self, request):
|
||||
"""Retrieve calendar list."""
|
||||
get_state = request.app['hass'].states.get
|
||||
calendar_list = []
|
||||
|
||||
for entity in self.component.entities:
|
||||
state = get_state(entity.entity_id)
|
||||
calendar_list.append({
|
||||
"name": state.name,
|
||||
"entity_id": entity.entity_id,
|
||||
})
|
||||
|
||||
return self.json(sorted(calendar_list, key=lambda x: x['name']))
|
||||
|
||||
@@ -11,7 +11,7 @@ import re
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.calendar import (
|
||||
PLATFORM_SCHEMA, CalendarEventDevice)
|
||||
PLATFORM_SCHEMA, CalendarEventDevice, get_date)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -92,7 +92,7 @@ def setup_platform(hass, config, add_devices, disc_info=None):
|
||||
if not config.get(CONF_CUSTOM_CALENDARS):
|
||||
device_data = {
|
||||
CONF_NAME: calendar.name,
|
||||
CONF_DEVICE_ID: calendar.name
|
||||
CONF_DEVICE_ID: calendar.name,
|
||||
}
|
||||
calendar_devices.append(
|
||||
WebDavCalendarEventDevice(hass, device_data, calendar)
|
||||
@@ -120,6 +120,10 @@ class WebDavCalendarEventDevice(CalendarEventDevice):
|
||||
attributes = super().device_state_attributes
|
||||
return attributes
|
||||
|
||||
async def async_get_events(self, hass, start_date, end_date):
|
||||
"""Get all events in a specific time frame."""
|
||||
return await self.data.async_get_events(hass, start_date, end_date)
|
||||
|
||||
|
||||
class WebDavCalendarData(object):
|
||||
"""Class to utilize the calendar dav client object to get next event."""
|
||||
@@ -131,6 +135,33 @@ class WebDavCalendarData(object):
|
||||
self.search = search
|
||||
self.event = None
|
||||
|
||||
async def async_get_events(self, hass, start_date, end_date):
|
||||
"""Get all events in a specific time frame."""
|
||||
# Get event list from the current calendar
|
||||
vevent_list = await hass.async_add_job(self.calendar.date_search,
|
||||
start_date, end_date)
|
||||
event_list = []
|
||||
for event in vevent_list:
|
||||
vevent = event.instance.vevent
|
||||
uid = None
|
||||
if hasattr(vevent, 'uid'):
|
||||
uid = vevent.uid.value
|
||||
data = {
|
||||
"uid": uid,
|
||||
"title": vevent.summary.value,
|
||||
"start": self.get_hass_date(vevent.dtstart.value),
|
||||
"end": self.get_hass_date(self.get_end_date(vevent)),
|
||||
"location": self.get_attr_value(vevent, "location"),
|
||||
"description": self.get_attr_value(vevent, "description"),
|
||||
}
|
||||
|
||||
data['start'] = get_date(data['start']).isoformat()
|
||||
data['end'] = get_date(data['end']).isoformat()
|
||||
|
||||
event_list.append(data)
|
||||
|
||||
return event_list
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
|
||||
@@ -4,8 +4,10 @@ Demo platform that has two fake binary sensors.
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import copy
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.calendar import CalendarEventDevice
|
||||
from homeassistant.components.calendar import CalendarEventDevice, get_date
|
||||
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
|
||||
|
||||
|
||||
@@ -15,13 +17,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
calendar_data_current = DemoGoogleCalendarDataCurrent()
|
||||
add_devices([
|
||||
DemoGoogleCalendar(hass, calendar_data_future, {
|
||||
CONF_NAME: 'Future Event',
|
||||
CONF_DEVICE_ID: 'future_event',
|
||||
CONF_NAME: 'Calendar 1',
|
||||
CONF_DEVICE_ID: 'calendar_1',
|
||||
}),
|
||||
|
||||
DemoGoogleCalendar(hass, calendar_data_current, {
|
||||
CONF_NAME: 'Current Event',
|
||||
CONF_DEVICE_ID: 'current_event',
|
||||
CONF_NAME: 'Calendar 2',
|
||||
CONF_DEVICE_ID: 'calendar_2',
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -29,11 +31,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class DemoGoogleCalendarData(object):
|
||||
"""Representation of a Demo Calendar element."""
|
||||
|
||||
event = {}
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def update(self):
|
||||
"""Return true so entity knows we have new data."""
|
||||
return True
|
||||
|
||||
async def async_get_events(self, hass, start_date, end_date):
|
||||
"""Get all events in a specific time frame."""
|
||||
event = copy.copy(self.event)
|
||||
event['title'] = event['summary']
|
||||
event['start'] = get_date(event['start']).isoformat()
|
||||
event['end'] = get_date(event['end']).isoformat()
|
||||
return [event]
|
||||
|
||||
|
||||
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
|
||||
"""Representation of a Demo Calendar for a future event."""
|
||||
@@ -80,3 +92,7 @@ class DemoGoogleCalendar(CalendarEventDevice):
|
||||
"""Initialize Google Calendar but without the API calls."""
|
||||
self.data = calendar_data
|
||||
super().__init__(hass, data)
|
||||
|
||||
async def async_get_events(self, hass, start_date, end_date):
|
||||
"""Get all events in a specific time frame."""
|
||||
return await self.data.async_get_events(hass, start_date, end_date)
|
||||
|
||||
@@ -51,6 +51,10 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
|
||||
|
||||
super().__init__(hass, data)
|
||||
|
||||
async def async_get_events(self, hass, start_date, end_date):
|
||||
"""Get all events in a specific time frame."""
|
||||
return await self.data.async_get_events(hass, start_date, end_date)
|
||||
|
||||
|
||||
class GoogleCalendarData(object):
|
||||
"""Class to utilize calendar service object to get next event."""
|
||||
@@ -64,9 +68,7 @@ class GoogleCalendarData(object):
|
||||
self.ignore_availability = ignore_availability
|
||||
self.event = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
def _prepare_query(self):
|
||||
from httplib2 import ServerNotFoundError
|
||||
|
||||
try:
|
||||
@@ -74,13 +76,41 @@ class GoogleCalendarData(object):
|
||||
except ServerNotFoundError:
|
||||
_LOGGER.warning("Unable to connect to Google, using cached data")
|
||||
return False
|
||||
|
||||
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
|
||||
params['timeMin'] = dt.now().isoformat('T')
|
||||
params['calendarId'] = self.calendar_id
|
||||
if self.search:
|
||||
params['q'] = self.search
|
||||
|
||||
return service, params
|
||||
|
||||
async def async_get_events(self, hass, start_date, end_date):
|
||||
"""Get all events in a specific time frame."""
|
||||
service, params = await hass.async_add_job(self._prepare_query)
|
||||
params['timeMin'] = start_date.isoformat('T')
|
||||
params['timeMax'] = end_date.isoformat('T')
|
||||
|
||||
# pylint: disable=no-member
|
||||
events = await hass.async_add_job(service.events)
|
||||
# pylint: enable=no-member
|
||||
result = await hass.async_add_job(events.list(**params).execute)
|
||||
|
||||
items = result.get('items', [])
|
||||
event_list = []
|
||||
for item in items:
|
||||
if (not self.ignore_availability
|
||||
and 'transparency' in item.keys()):
|
||||
if item['transparency'] == 'opaque':
|
||||
event_list.append(item)
|
||||
else:
|
||||
event_list.append(item)
|
||||
return event_list
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
service, params = self._prepare_query()
|
||||
params['timeMin'] = dt.now().isoformat('T')
|
||||
|
||||
events = service.events() # pylint: disable=no-member
|
||||
result = events.list(**params).execute()
|
||||
|
||||
|
||||
@@ -257,6 +257,10 @@ class TodoistProjectDevice(CalendarEventDevice):
|
||||
super().cleanup()
|
||||
self._cal_data[ALL_TASKS] = []
|
||||
|
||||
async def async_get_events(self, hass, start_date, end_date):
|
||||
"""Get all events in a specific time frame."""
|
||||
return await self.data.async_get_events(hass, start_date, end_date)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
@@ -485,6 +489,31 @@ class TodoistProjectData(object):
|
||||
continue
|
||||
return event
|
||||
|
||||
async def async_get_events(self, hass, start_date, end_date):
|
||||
"""Get all tasks in a specific time frame."""
|
||||
if self._id is None:
|
||||
project_task_data = [
|
||||
task for task in self._api.state[TASKS]
|
||||
if not self._project_id_whitelist or
|
||||
task[PROJECT_ID] in self._project_id_whitelist]
|
||||
else:
|
||||
project_task_data = self._api.projects.get_data(self._id)[TASKS]
|
||||
|
||||
events = []
|
||||
time_format = '%a %d %b %Y %H:%M:%S %z'
|
||||
for task in project_task_data:
|
||||
due_date = datetime.strptime(task['due_date_utc'], time_format)
|
||||
if due_date > start_date and due_date < end_date:
|
||||
event = {
|
||||
'uid': task['id'],
|
||||
'title': task['content'],
|
||||
'start': due_date.isoformat(),
|
||||
'end': due_date.isoformat(),
|
||||
'allDay': True,
|
||||
}
|
||||
events.append(event)
|
||||
return events
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# pylint: disable=too-many-lines
|
||||
"""
|
||||
Component to interface with cameras.
|
||||
|
||||
@@ -97,6 +96,7 @@ def disable_motion_detection(hass, entity_id=None):
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_snapshot(hass, filename, entity_id=None):
|
||||
"""Make a snapshot from a camera."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
@@ -129,8 +129,7 @@ async def async_get_image(hass, entity_id, timeout=10):
|
||||
raise HomeAssistantError('Unable to get image')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the camera component."""
|
||||
component = hass.data[DOMAIN] = \
|
||||
EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
@@ -142,7 +141,7 @@ def async_setup(hass, config):
|
||||
SCHEMA_WS_CAMERA_THUMBNAIL
|
||||
)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
await component.async_setup(config)
|
||||
|
||||
@callback
|
||||
def update_tokens(time):
|
||||
@@ -154,27 +153,25 @@ def async_setup(hass, config):
|
||||
hass.helpers.event.async_track_time_interval(
|
||||
update_tokens, TOKEN_CHANGE_INTERVAL)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_camera_service(service):
|
||||
async def async_handle_camera_service(service):
|
||||
"""Handle calls to the camera services."""
|
||||
target_cameras = component.async_extract_from_service(service)
|
||||
|
||||
update_tasks = []
|
||||
for camera in target_cameras:
|
||||
if service.service == SERVICE_ENABLE_MOTION:
|
||||
yield from camera.async_enable_motion_detection()
|
||||
await camera.async_enable_motion_detection()
|
||||
elif service.service == SERVICE_DISABLE_MOTION:
|
||||
yield from camera.async_disable_motion_detection()
|
||||
await camera.async_disable_motion_detection()
|
||||
|
||||
if not camera.should_poll:
|
||||
continue
|
||||
update_tasks.append(camera.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_snapshot_service(service):
|
||||
async def async_handle_snapshot_service(service):
|
||||
"""Handle snapshot services calls."""
|
||||
target_cameras = component.async_extract_from_service(service)
|
||||
filename = service.data[ATTR_FILENAME]
|
||||
@@ -190,7 +187,7 @@ def async_setup(hass, config):
|
||||
"Can't write %s, no access to path!", snapshot_file)
|
||||
continue
|
||||
|
||||
image = yield from camera.async_camera_image()
|
||||
image = await camera.async_camera_image()
|
||||
|
||||
def _write_image(to_file, image_data):
|
||||
"""Executor helper to write image."""
|
||||
@@ -198,7 +195,7 @@ def async_setup(hass, config):
|
||||
img_file.write(image_data)
|
||||
|
||||
try:
|
||||
yield from hass.async_add_job(
|
||||
await hass.async_add_job(
|
||||
_write_image, snapshot_file, image)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't write image to file: %s", err)
|
||||
@@ -216,6 +213,16 @@ def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Setup a config entry."""
|
||||
return await hass.data[DOMAIN].async_setup_entry(entry)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||
|
||||
|
||||
class Camera(Entity):
|
||||
"""The base class for camera entities."""
|
||||
|
||||
@@ -265,6 +272,7 @@ class Camera(Entity):
|
||||
"""Return bytes of camera image."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@callback
|
||||
def async_camera_image(self):
|
||||
"""Return bytes of camera image.
|
||||
|
||||
@@ -388,8 +396,7 @@ class CameraView(HomeAssistantView):
|
||||
"""Initialize a basic camera view."""
|
||||
self.component = component
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, entity_id):
|
||||
async def get(self, request, entity_id):
|
||||
"""Start a GET request."""
|
||||
camera = self.component.get_entity(entity_id)
|
||||
|
||||
@@ -403,11 +410,10 @@ class CameraView(HomeAssistantView):
|
||||
if not authenticated:
|
||||
return web.Response(status=401)
|
||||
|
||||
response = yield from self.handle(request, camera)
|
||||
response = await self.handle(request, camera)
|
||||
return response
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle(self, request, camera):
|
||||
async def handle(self, request, camera):
|
||||
"""Handle the camera request."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -418,12 +424,11 @@ class CameraImageView(CameraView):
|
||||
url = '/api/camera_proxy/{entity_id}'
|
||||
name = 'api:camera:image'
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle(self, request, camera):
|
||||
async def handle(self, request, camera):
|
||||
"""Serve camera image."""
|
||||
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
||||
with async_timeout.timeout(10, loop=request.app['hass'].loop):
|
||||
image = yield from camera.async_camera_image()
|
||||
image = await camera.async_camera_image()
|
||||
|
||||
if image:
|
||||
return web.Response(body=image,
|
||||
|
||||
@@ -4,23 +4,22 @@ Support for Netgear Arlo IP cameras.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.arlo/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO
|
||||
from homeassistant.components.arlo import (
|
||||
DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO)
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=90)
|
||||
|
||||
ARLO_MODE_ARMED = 'armed'
|
||||
ARLO_MODE_DISARMED = 'disarmed'
|
||||
|
||||
@@ -44,22 +43,19 @@ POWERSAVE_MODE_MAPPING = {
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS):
|
||||
cv.string,
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up an Arlo IP Camera."""
|
||||
arlo = hass.data.get(DATA_ARLO)
|
||||
if not arlo:
|
||||
return False
|
||||
arlo = hass.data[DATA_ARLO]
|
||||
|
||||
cameras = []
|
||||
for camera in arlo.cameras:
|
||||
cameras.append(ArloCam(hass, camera, config))
|
||||
|
||||
add_devices(cameras, True)
|
||||
add_devices(cameras)
|
||||
|
||||
|
||||
class ArloCam(Camera):
|
||||
@@ -74,31 +70,41 @@ class ArloCam(Camera):
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self._last_refresh = None
|
||||
if self._camera.base_station:
|
||||
self._camera.base_station.refresh_rate = \
|
||||
SCAN_INTERVAL.total_seconds()
|
||||
self.attrs = {}
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
return self._camera.last_image
|
||||
return self._camera.last_image_from_cache
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
video = self._camera.last_video
|
||||
if not video:
|
||||
error_msg = \
|
||||
'Video not found for {0}. Is it older than {1} days?'.format(
|
||||
self.name, self._camera.min_days_vdo_cache)
|
||||
_LOGGER.error(error_msg)
|
||||
return
|
||||
|
||||
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
await stream.open_camera(
|
||||
video.video_url, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
await stream.close()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -132,11 +138,6 @@ class ArloCam(Camera):
|
||||
"""Return the camera brand."""
|
||||
return DEFAULT_BRAND
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Camera should poll periodically."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
@@ -164,7 +165,3 @@ class ArloCam(Camera):
|
||||
"""Disable the motion detection in base station (Disarm)."""
|
||||
self._motion_status = False
|
||||
self.set_base_station_mode(ARLO_MODE_DISARMED)
|
||||
|
||||
def update(self):
|
||||
"""Add an attribute-update task to the executor pool."""
|
||||
self._camera.update()
|
||||
|
||||
@@ -13,7 +13,6 @@ from homeassistant.components.camera import Camera
|
||||
DEPENDENCIES = ['bloomsky']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up access to BloomSky cameras."""
|
||||
bloomsky = hass.components.bloomsky
|
||||
|
||||
@@ -12,9 +12,10 @@ from homeassistant.components.camera import Camera
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the Demo camera platform."""
|
||||
add_devices([
|
||||
async_add_devices([
|
||||
DemoCamera(hass, config, 'Demo camera')
|
||||
])
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
DEPENDENCIES = ['doorbird']
|
||||
|
||||
_CAMERA_LAST_VISITOR = "DoorBird Last Ring"
|
||||
_CAMERA_LAST_MOTION = "DoorBird Last Motion"
|
||||
_CAMERA_LIVE = "DoorBird Live"
|
||||
_CAMERA_LAST_VISITOR = "{} Last Ring"
|
||||
_CAMERA_LAST_MOTION = "{} Last Motion"
|
||||
_CAMERA_LIVE = "{} Live"
|
||||
_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1)
|
||||
_LAST_MOTION_INTERVAL = datetime.timedelta(minutes=1)
|
||||
_LIVE_INTERVAL = datetime.timedelta(seconds=1)
|
||||
@@ -30,16 +30,22 @@ _TIMEOUT = 10 # seconds
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the DoorBird camera platform."""
|
||||
device = hass.data.get(DOORBIRD_DOMAIN)
|
||||
async_add_devices([
|
||||
DoorBirdCamera(device.live_image_url, _CAMERA_LIVE, _LIVE_INTERVAL),
|
||||
DoorBirdCamera(
|
||||
device.history_image_url(1, 'doorbell'), _CAMERA_LAST_VISITOR,
|
||||
_LAST_VISITOR_INTERVAL),
|
||||
DoorBirdCamera(
|
||||
device.history_image_url(1, 'motionsensor'), _CAMERA_LAST_MOTION,
|
||||
_LAST_MOTION_INTERVAL),
|
||||
])
|
||||
for doorstation in hass.data[DOORBIRD_DOMAIN]:
|
||||
device = doorstation.device
|
||||
async_add_devices([
|
||||
DoorBirdCamera(
|
||||
device.live_image_url,
|
||||
_CAMERA_LIVE.format(doorstation.name),
|
||||
_LIVE_INTERVAL),
|
||||
DoorBirdCamera(
|
||||
device.history_image_url(1, 'doorbell'),
|
||||
_CAMERA_LAST_VISITOR.format(doorstation.name),
|
||||
_LAST_VISITOR_INTERVAL),
|
||||
DoorBirdCamera(
|
||||
device.history_image_url(1, 'motionsensor'),
|
||||
_CAMERA_LAST_MOTION.format(doorstation.name),
|
||||
_LAST_MOTION_INTERVAL),
|
||||
])
|
||||
|
||||
|
||||
class DoorBirdCamera(Camera):
|
||||
|
||||
@@ -33,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a Foscam IP Camera."""
|
||||
add_devices([FoscamCam(config)])
|
||||
|
||||
@@ -46,7 +46,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up a generic IP Camera."""
|
||||
async_add_devices([GenericCamera(hass, config)])
|
||||
|
||||
@@ -42,7 +42,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up a MJPEG IP Camera."""
|
||||
if discovery_info:
|
||||
|
||||
@@ -23,14 +23,19 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a Nest Cam."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
"""Set up a Nest Cam.
|
||||
|
||||
camera_devices = hass.data[nest.DATA_NEST].cameras()
|
||||
No longer in use.
|
||||
"""
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_devices):
|
||||
"""Set up a Nest sensor based on a config entry."""
|
||||
camera_devices = \
|
||||
await hass.async_add_job(hass.data[nest.DATA_NEST].cameras)
|
||||
cameras = [NestCamera(structure, device)
|
||||
for structure, device in camera_devices]
|
||||
add_devices(cameras, True)
|
||||
async_add_devices(cameras, True)
|
||||
|
||||
|
||||
class NestCamera(Camera):
|
||||
|
||||
@@ -29,13 +29,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up access to Netatmo cameras."""
|
||||
netatmo = hass.components.netatmo
|
||||
home = config.get(CONF_HOME)
|
||||
verify_ssl = config.get(CONF_VERIFY_SSL, True)
|
||||
import lnetatmo
|
||||
import pyatmo
|
||||
try:
|
||||
data = CameraData(netatmo.NETATMO_AUTH, home)
|
||||
for camera_name in data.get_camera_names():
|
||||
@@ -46,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
continue
|
||||
add_devices([NetatmoCamera(data, camera_name, home,
|
||||
camera_type, verify_ssl)])
|
||||
except lnetatmo.NoDevice:
|
||||
except pyatmo.NoDevice:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_PORT
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
|
||||
REQUIREMENTS = ['uvcclient==0.10.1']
|
||||
|
||||
@@ -41,25 +42,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
port = config[CONF_PORT]
|
||||
|
||||
from uvcclient import nvr
|
||||
nvrconn = nvr.UVCRemote(addr, port, key)
|
||||
try:
|
||||
# Exceptions may be raised in all method calls to the nvr library.
|
||||
nvrconn = nvr.UVCRemote(addr, port, key)
|
||||
cameras = nvrconn.index()
|
||||
|
||||
identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'
|
||||
# Filter out airCam models, which are not supported in the latest
|
||||
# version of UnifiVideo and which are EOL by Ubiquiti
|
||||
cameras = [
|
||||
camera for camera in cameras
|
||||
if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']]
|
||||
except nvr.NotAuthorized:
|
||||
_LOGGER.error("Authorization failure while connecting to NVR")
|
||||
return False
|
||||
except nvr.NvrError:
|
||||
_LOGGER.error("NVR refuses to talk to me")
|
||||
return False
|
||||
except nvr.NvrError as ex:
|
||||
_LOGGER.error("NVR refuses to talk to me: %s", str(ex))
|
||||
raise PlatformNotReady
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to NVR: %s", str(ex))
|
||||
return False
|
||||
|
||||
identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'
|
||||
# Filter out airCam models, which are not supported in the latest
|
||||
# version of UnifiVideo and which are EOL by Ubiquiti
|
||||
cameras = [
|
||||
camera for camera in cameras
|
||||
if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']]
|
||||
raise PlatformNotReady
|
||||
|
||||
add_devices([UnifiVideoCamera(nvrconn,
|
||||
camera[identifier],
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
This component provides support for Xiaomi Cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.xiaomi/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PATH,
|
||||
CONF_PASSWORD, CONF_PORT, CONF_USERNAME)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_BRAND = 'Xiaomi Home Camera'
|
||||
DEFAULT_PATH = '/media/mmcblk0p1/record'
|
||||
DEFAULT_PORT = 21
|
||||
DEFAULT_USERNAME = 'root'
|
||||
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
CONF_MODEL = 'model'
|
||||
|
||||
MODEL_YI = 'yi'
|
||||
MODEL_XIAOFANG = 'xiaofang'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_MODEL): vol.Any(MODEL_YI,
|
||||
MODEL_XIAOFANG),
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string,
|
||||
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass,
|
||||
config,
|
||||
async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up a Xiaomi Camera."""
|
||||
_LOGGER.debug('Received configuration for model %s', config[CONF_MODEL])
|
||||
async_add_devices([XiaomiCamera(hass, config)])
|
||||
|
||||
|
||||
class XiaomiCamera(Camera):
|
||||
"""Define an implementation of a Xiaomi Camera."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self._last_image = None
|
||||
self._last_url = None
|
||||
self._manager = hass.data[DATA_FFMPEG]
|
||||
self._name = config[CONF_NAME]
|
||||
self.host = config[CONF_HOST]
|
||||
self._model = config[CONF_MODEL]
|
||||
self.port = config[CONF_PORT]
|
||||
self.path = config[CONF_PATH]
|
||||
self.user = config[CONF_USERNAME]
|
||||
self.passwd = config[CONF_PASSWORD]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Return the camera brand."""
|
||||
return DEFAULT_BRAND
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Return the camera model."""
|
||||
return self._model
|
||||
|
||||
def get_latest_video_url(self):
|
||||
"""Retrieve the latest video file from the Xiaomi Camera FTP server."""
|
||||
from ftplib import FTP, error_perm
|
||||
|
||||
ftp = FTP(self.host)
|
||||
try:
|
||||
ftp.login(self.user, self.passwd)
|
||||
except error_perm as exc:
|
||||
_LOGGER.error('Camera login failed: %s', exc)
|
||||
return False
|
||||
|
||||
try:
|
||||
ftp.cwd(self.path)
|
||||
except error_perm as exc:
|
||||
_LOGGER.error('Unable to find path: %s - %s', self.path, exc)
|
||||
return False
|
||||
|
||||
dirs = [d for d in ftp.nlst() if '.' not in d]
|
||||
if not dirs:
|
||||
if self._model == MODEL_YI:
|
||||
_LOGGER.warning("There don't appear to be any uploaded videos")
|
||||
return False
|
||||
elif self._model == MODEL_XIAOFANG:
|
||||
_LOGGER.warning("There don't appear to be any folders")
|
||||
return False
|
||||
|
||||
first_dir = dirs[-1]
|
||||
try:
|
||||
ftp.cwd(first_dir)
|
||||
except error_perm as exc:
|
||||
_LOGGER.error('Unable to find path: %s - %s', first_dir, exc)
|
||||
return False
|
||||
|
||||
dirs = [d for d in ftp.nlst() if '.' not in d]
|
||||
if not dirs:
|
||||
_LOGGER.warning("There don't appear to be any uploaded videos")
|
||||
return False
|
||||
|
||||
latest_dir = dirs[-1]
|
||||
ftp.cwd(latest_dir)
|
||||
videos = [v for v in ftp.nlst() if '.tmp' not in v]
|
||||
if not videos:
|
||||
_LOGGER.info('Video folder "%s" is empty; delaying', latest_dir)
|
||||
return False
|
||||
|
||||
if self._model == MODEL_XIAOFANG:
|
||||
video = videos[-2]
|
||||
else:
|
||||
video = videos[-1]
|
||||
|
||||
return 'ftp://{0}:{1}@{2}:{3}{4}/{5}'.format(
|
||||
self.user, self.passwd, self.host, self.port, ftp.pwd(), video)
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
|
||||
url = await self.hass.async_add_job(self.get_latest_video_url)
|
||||
if url != self._last_url:
|
||||
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
|
||||
self._last_image = await asyncio.shield(ffmpeg.get_image(
|
||||
url, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._extra_arguments), loop=self.hass.loop)
|
||||
self._last_url = url
|
||||
|
||||
return self._last_image
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
|
||||
await stream.open_camera(
|
||||
self._last_url, extra_cmd=self._extra_arguments)
|
||||
|
||||
await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
await stream.close()
|
||||
@@ -11,11 +11,13 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PATH,
|
||||
CONF_PASSWORD, CONF_PORT, CONF_USERNAME)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PATH, CONF_PASSWORD, CONF_PORT, CONF_USERNAME)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
|
||||
REQUIREMENTS = ['aioftp==0.10.1']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,12 +40,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass,
|
||||
config,
|
||||
async_add_devices,
|
||||
discovery_info=None):
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up a Yi Camera."""
|
||||
_LOGGER.debug('Received configuration: %s', config)
|
||||
async_add_devices([YiCamera(hass, config)], True)
|
||||
|
||||
|
||||
@@ -54,71 +53,81 @@ class YiCamera(Camera):
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self._ftp = None
|
||||
self._last_image = None
|
||||
self._last_url = None
|
||||
self._manager = hass.data[DATA_FFMPEG]
|
||||
self._name = config.get(CONF_NAME)
|
||||
self.host = config.get(CONF_HOST)
|
||||
self.port = config.get(CONF_PORT)
|
||||
self.path = config.get(CONF_PATH)
|
||||
self.user = config.get(CONF_USERNAME)
|
||||
self.passwd = config.get(CONF_PASSWORD)
|
||||
self._name = config[CONF_NAME]
|
||||
self.host = config[CONF_HOST]
|
||||
self.port = config[CONF_PORT]
|
||||
self.path = config[CONF_PATH]
|
||||
self.user = config[CONF_USERNAME]
|
||||
self.passwd = config[CONF_PASSWORD]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
hass.async_add_job(self._connect_to_client)
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Camera brand."""
|
||||
return DEFAULT_BRAND
|
||||
|
||||
def get_latest_video_url(self):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
async def _connect_to_client(self):
|
||||
"""Attempt to establish a connection via FTP."""
|
||||
from aioftp import Client, StatusCodeError
|
||||
|
||||
ftp = Client()
|
||||
try:
|
||||
await ftp.connect(self.host)
|
||||
await ftp.login(self.user, self.passwd)
|
||||
self._ftp = ftp
|
||||
except StatusCodeError as err:
|
||||
raise PlatformNotReady(err)
|
||||
|
||||
async def _get_latest_video_url(self):
|
||||
"""Retrieve the latest video file from the customized Yi FTP server."""
|
||||
from ftplib import FTP, error_perm
|
||||
|
||||
ftp = FTP(self.host)
|
||||
try:
|
||||
ftp.login(self.user, self.passwd)
|
||||
except error_perm as exc:
|
||||
_LOGGER.error('There was an error while logging into the camera')
|
||||
_LOGGER.debug(exc)
|
||||
return False
|
||||
from aioftp import StatusCodeError
|
||||
|
||||
try:
|
||||
ftp.cwd(self.path)
|
||||
except error_perm as exc:
|
||||
_LOGGER.error('Unable to find path: %s', self.path)
|
||||
_LOGGER.debug(exc)
|
||||
return False
|
||||
await self._ftp.change_directory(self.path)
|
||||
dirs = []
|
||||
for path, attrs in await self._ftp.list():
|
||||
if attrs['type'] == 'dir' and '.' not in str(path):
|
||||
dirs.append(path)
|
||||
latest_dir = dirs[-1]
|
||||
await self._ftp.change_directory(latest_dir)
|
||||
|
||||
dirs = [d for d in ftp.nlst() if '.' not in d]
|
||||
if not dirs:
|
||||
_LOGGER.warning("There don't appear to be any uploaded videos")
|
||||
return False
|
||||
videos = []
|
||||
for path, _ in await self._ftp.list():
|
||||
videos.append(path)
|
||||
if not videos:
|
||||
_LOGGER.info('Video folder "%s" empty; delaying', latest_dir)
|
||||
return None
|
||||
|
||||
latest_dir = dirs[-1]
|
||||
ftp.cwd(latest_dir)
|
||||
videos = ftp.nlst()
|
||||
if not videos:
|
||||
_LOGGER.info('Video folder "%s" is empty; delaying', latest_dir)
|
||||
return False
|
||||
|
||||
return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format(
|
||||
self.user, self.passwd, self.host, self.port, self.path,
|
||||
latest_dir, videos[-1])
|
||||
return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format(
|
||||
self.user, self.passwd, self.host, self.port, self.path,
|
||||
latest_dir, videos[-1])
|
||||
except (ConnectionRefusedError, StatusCodeError) as err:
|
||||
_LOGGER.error('Error while fetching video: %s', err)
|
||||
return None
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
|
||||
url = await self.hass.async_add_job(self.get_latest_video_url)
|
||||
url = await self._get_latest_video_url()
|
||||
if url != self._last_url:
|
||||
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
|
||||
self._last_image = await asyncio.shield(ffmpeg.get_image(
|
||||
url, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._extra_arguments), loop=self.hass.loop)
|
||||
self._last_image = await asyncio.shield(
|
||||
ffmpeg.get_image(
|
||||
url,
|
||||
output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._extra_arguments),
|
||||
loop=self.hass.loop)
|
||||
self._last_url = url
|
||||
|
||||
return self._last_image
|
||||
|
||||
@@ -49,7 +49,6 @@ def _get_image_url(hass, monitor, mode):
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the ZoneMinder cameras."""
|
||||
cameras = []
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "No Google Cast devices found on the network.",
|
||||
"single_instance_allowed": "Only a single configuration of Google Cast is necessary."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Do you want to setup Google Cast?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Component to embed Google Cast."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
|
||||
|
||||
DOMAIN = 'cast'
|
||||
REQUIREMENTS = ['pychromecast==2.1.0']
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the Cast component."""
|
||||
hass.data[DOMAIN] = config.get(DOMAIN, {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up Cast from a config entry."""
|
||||
hass.async_add_job(hass.config_entries.async_forward_entry_setup(
|
||||
entry, 'media_player'))
|
||||
return True
|
||||
|
||||
|
||||
async def _async_has_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
from pychromecast.discovery import discover_chromecasts
|
||||
|
||||
return await hass.async_add_job(discover_chromecasts)
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, 'Google Cast', _async_has_devices)
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "Google Cast",
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Google Cast",
|
||||
"description": "Do you want to setup Google Cast?"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only a single configuration of Google Cast is necessary.",
|
||||
"no_devices_found": "No Google Cast devices found on the network."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,7 +246,8 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up climate devices."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
component = hass.data[DOMAIN] = \
|
||||
EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
await component.async_setup(config)
|
||||
|
||||
async def async_away_mode_set_service(service):
|
||||
@@ -456,6 +457,16 @@ async def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Setup a config entry."""
|
||||
return await hass.data[DOMAIN].async_setup_entry(entry)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||
|
||||
|
||||
class ClimateDevice(Entity):
|
||||
"""Representation of a climate device."""
|
||||
|
||||
|
||||
@@ -13,21 +13,27 @@ from homeassistant.components.fritzbox import (
|
||||
ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_LOCKED)
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
|
||||
STATE_OFF, STATE_ON, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS)
|
||||
|
||||
DEPENDENCIES = ['fritzbox']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE)
|
||||
|
||||
OPERATION_LIST = [STATE_HEAT, STATE_ECO]
|
||||
OPERATION_LIST = [STATE_HEAT, STATE_ECO, STATE_OFF, STATE_ON]
|
||||
|
||||
MIN_TEMPERATURE = 8
|
||||
MAX_TEMPERATURE = 28
|
||||
|
||||
# special temperatures for on/off in Fritz!Box API (modified by pyfritzhome)
|
||||
ON_API_TEMPERATURE = 127.0
|
||||
OFF_API_TEMPERATURE = 126.5
|
||||
ON_REPORT_SET_TEMPERATURE = 30.0
|
||||
OFF_REPORT_SET_TEMPERATURE = 0.0
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Fritzbox smarthome thermostat platform."""
|
||||
@@ -88,6 +94,9 @@ class FritzboxThermostat(ClimateDevice):
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._target_temperature in (ON_API_TEMPERATURE,
|
||||
OFF_API_TEMPERATURE):
|
||||
return None
|
||||
return self._target_temperature
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
@@ -102,9 +111,13 @@ class FritzboxThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return the current operation mode."""
|
||||
if self._target_temperature == ON_API_TEMPERATURE:
|
||||
return STATE_ON
|
||||
if self._target_temperature == OFF_API_TEMPERATURE:
|
||||
return STATE_OFF
|
||||
if self._target_temperature == self._comfort_temperature:
|
||||
return STATE_HEAT
|
||||
elif self._target_temperature == self._eco_temperature:
|
||||
if self._target_temperature == self._eco_temperature:
|
||||
return STATE_ECO
|
||||
return STATE_MANUAL
|
||||
|
||||
@@ -119,6 +132,10 @@ class FritzboxThermostat(ClimateDevice):
|
||||
self.set_temperature(temperature=self._comfort_temperature)
|
||||
elif operation_mode == STATE_ECO:
|
||||
self.set_temperature(temperature=self._eco_temperature)
|
||||
elif operation_mode == STATE_OFF:
|
||||
self.set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
|
||||
elif operation_mode == STATE_ON:
|
||||
self.set_temperature(temperature=ON_REPORT_SET_TEMPERATURE)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
||||
@@ -14,8 +14,7 @@ from homeassistant.core import DOMAIN as HA_DOMAIN
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice,
|
||||
ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA,
|
||||
DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
|
||||
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
|
||||
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
@@ -268,7 +267,8 @@ class GenericThermostat(ClimateDevice):
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
|
||||
return DEFAULT_MIN_TEMP
|
||||
# get default temp from super class
|
||||
return super().min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
@@ -277,7 +277,8 @@ class GenericThermostat(ClimateDevice):
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
|
||||
return DEFAULT_MAX_TEMP
|
||||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_sensor_changed(self, entity_id, old_state, new_state):
|
||||
|
||||
@@ -136,7 +136,6 @@ class KNXClimate(ClimateDevice):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
await self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.components.climate import (
|
||||
PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO,
|
||||
ATTR_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE,
|
||||
SUPPORT_AUX_HEAT)
|
||||
SUPPORT_AUX_HEAT, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, CONF_VALUE_TEMPLATE)
|
||||
from homeassistant.components.mqtt import (
|
||||
@@ -70,6 +70,9 @@ CONF_SWING_MODE_LIST = 'swing_modes'
|
||||
CONF_INITIAL = 'initial'
|
||||
CONF_SEND_IF_OFF = 'send_if_off'
|
||||
|
||||
CONF_MIN_TEMP = 'min_temp'
|
||||
CONF_MAX_TEMP = 'max_temp'
|
||||
|
||||
SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema)
|
||||
PLATFORM_SCHEMA = SCHEMA_BASE.extend({
|
||||
vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
@@ -116,6 +119,10 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
|
||||
vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
|
||||
|
||||
vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float)
|
||||
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
@@ -181,19 +188,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
config.get(CONF_PAYLOAD_OFF),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE))
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
config.get(CONF_MIN_TEMP),
|
||||
config.get(CONF_MAX_TEMP))
|
||||
])
|
||||
|
||||
|
||||
class MqttClimate(MqttAvailability, ClimateDevice):
|
||||
"""Representation of a demo climate device."""
|
||||
"""Representation of an MQTT climate device."""
|
||||
|
||||
def __init__(self, hass, name, topic, value_templates, qos, retain,
|
||||
mode_list, fan_mode_list, swing_mode_list,
|
||||
target_temperature, away, hold, current_fan_mode,
|
||||
current_swing_mode, current_operation, aux, send_if_off,
|
||||
payload_on, payload_off, availability_topic,
|
||||
payload_available, payload_not_available):
|
||||
payload_available, payload_not_available,
|
||||
min_temp, max_temp):
|
||||
"""Initialize the climate device."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
payload_not_available)
|
||||
@@ -219,6 +229,8 @@ class MqttClimate(MqttAvailability, ClimateDevice):
|
||||
self._send_if_off = send_if_off
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
self._min_temp = min_temp
|
||||
self._max_temp = max_temp
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
@@ -619,3 +631,15 @@ class MqttClimate(MqttAvailability, ClimateDevice):
|
||||
support |= SUPPORT_AUX_HEAT
|
||||
|
||||
return support
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
# pylint: disable=no-member
|
||||
return self._min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
# pylint: disable=no-member
|
||||
return self._max_temp
|
||||
|
||||
@@ -32,16 +32,22 @@ NEST_MODE_HEAT_COOL = 'heat-cool'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Nest thermostat."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
"""Set up the Nest thermostat.
|
||||
|
||||
No longer in use.
|
||||
"""
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_devices):
|
||||
"""Set up the Nest climate device based on a config entry."""
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
|
||||
all_devices = [NestThermostat(structure, device, temp_unit)
|
||||
for structure, device in hass.data[DATA_NEST].thermostats()]
|
||||
thermostats = await hass.async_add_job(hass.data[DATA_NEST].thermostats)
|
||||
|
||||
add_devices(all_devices, True)
|
||||
all_devices = [NestThermostat(structure, device, temp_unit)
|
||||
for structure, device in thermostats]
|
||||
|
||||
async_add_devices(all_devices, True)
|
||||
|
||||
|
||||
class NestThermostat(ClimateDevice):
|
||||
|
||||
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
netatmo = hass.components.netatmo
|
||||
device = config.get(CONF_RELAY)
|
||||
|
||||
import lnetatmo
|
||||
import pyatmo
|
||||
try:
|
||||
data = ThermostatData(netatmo.NETATMO_AUTH, device)
|
||||
for module_name in data.get_module_names():
|
||||
@@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
module_name not in config[CONF_THERMOSTAT]:
|
||||
continue
|
||||
add_devices([NetatmoThermostat(data, module_name)], True)
|
||||
except lnetatmo.NoDevice:
|
||||
except pyatmo.NoDevice:
|
||||
return None
|
||||
|
||||
|
||||
@@ -168,8 +168,8 @@ class ThermostatData(object):
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Call the NetAtmo API to update the data."""
|
||||
import lnetatmo
|
||||
self.thermostatdata = lnetatmo.ThermostatData(self.auth)
|
||||
import pyatmo
|
||||
self.thermostatdata = pyatmo.ThermostatData(self.auth)
|
||||
self.target_temperature = self.thermostatdata.setpoint_temp
|
||||
self.setpoint_mode = self.thermostatdata.setpoint_mode
|
||||
self.current_temperature = self.thermostatdata.temp
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_FAN_MODE, SUPPORT_SWING_MODE,
|
||||
SUPPORT_ON_OFF, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
|
||||
SUPPORT_ON_OFF)
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -246,13 +246,13 @@ class SensiboClimate(ClimateDevice):
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._temperatures_list[0] \
|
||||
if self._temperatures_list else DEFAULT_MIN_TEMP
|
||||
if self._temperatures_list else super().min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._temperatures_list[-1] \
|
||||
if self._temperatures_list else DEFAULT_MAX_TEMP
|
||||
if self._temperatures_list else super().max_temp
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
||||
@@ -8,8 +8,8 @@ import logging
|
||||
|
||||
from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS)
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
|
||||
DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
|
||||
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
from homeassistant.components.tado import DATA_TADO
|
||||
|
||||
@@ -231,18 +231,14 @@ class TadoClimate(ClimateDevice):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
|
||||
return DEFAULT_MIN_TEMP
|
||||
return convert_temperature(self._min_temp, self._unit,
|
||||
self.hass.config.units.temperature_unit)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
|
||||
return DEFAULT_MAX_TEMP
|
||||
return convert_temperature(self._max_temp, self._unit,
|
||||
self.hass.config.units.temperature_unit)
|
||||
|
||||
def update(self):
|
||||
"""Update the state of this climate device."""
|
||||
|
||||
@@ -32,8 +32,8 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Set up of Vera thermostats."""
|
||||
add_devices_callback(
|
||||
VeraThermostat(device, hass.data[VERA_CONTROLLER]) for
|
||||
device in hass.data[VERA_DEVICES]['climate'])
|
||||
[VeraThermostat(device, hass.data[VERA_CONTROLLER]) for
|
||||
device in hass.data[VERA_DEVICES]['climate']], True)
|
||||
|
||||
|
||||
class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
@@ -101,10 +101,6 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
if power:
|
||||
return convert(power, float, 0.0)
|
||||
|
||||
def update(self):
|
||||
"""Handle state updates."""
|
||||
self._state = self.vera_device.get_hvac_mode()
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
|
||||
@@ -84,7 +84,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices([WinkWaterHeater(water_heater, hass)])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink thermostat."""
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
Support for ZhongHong HVAC Controller.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.zhong_hong/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_OPERATION_MODE, PLATFORM_SCHEMA, STATE_COOL, STATE_DRY,
|
||||
STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice)
|
||||
from homeassistant.const import (ATTR_TEMPERATURE, CONF_HOST, CONF_PORT,
|
||||
EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (async_dispatcher_connect,
|
||||
async_dispatcher_send)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_GATEWAY_ADDRRESS = 'gateway_address'
|
||||
|
||||
REQUIREMENTS = ['zhong_hong_hvac==1.0.9']
|
||||
SIGNAL_DEVICE_ADDED = 'zhong_hong_device_added'
|
||||
SIGNAL_ZHONG_HONG_HUB_START = 'zhong_hong_hub_start'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST):
|
||||
cv.string,
|
||||
vol.Optional(CONF_PORT, default=9999):
|
||||
vol.Coerce(int),
|
||||
vol.Optional(CONF_GATEWAY_ADDRRESS, default=1):
|
||||
vol.Coerce(int),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the ZhongHong HVAC platform."""
|
||||
from zhong_hong_hvac.hub import ZhongHongGateway
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
gw_addr = config.get(CONF_GATEWAY_ADDRRESS)
|
||||
hub = ZhongHongGateway(host, port, gw_addr)
|
||||
devices = [
|
||||
ZhongHongClimate(hub, addr_out, addr_in)
|
||||
for (addr_out, addr_in) in hub.discovery_ac()
|
||||
]
|
||||
|
||||
_LOGGER.debug("We got %s zhong_hong climate devices", len(devices))
|
||||
|
||||
hub_is_initialized = False
|
||||
|
||||
async def startup():
|
||||
"""Start hub socket after all climate entity is setted up."""
|
||||
nonlocal hub_is_initialized
|
||||
if not all([device.is_initialized for device in devices]):
|
||||
return
|
||||
|
||||
if hub_is_initialized:
|
||||
return
|
||||
|
||||
_LOGGER.debug("zhong_hong hub start listen event")
|
||||
await hass.async_add_job(hub.start_listen)
|
||||
await hass.async_add_job(hub.query_all_status)
|
||||
hub_is_initialized = True
|
||||
|
||||
async_dispatcher_connect(hass, SIGNAL_DEVICE_ADDED, startup)
|
||||
|
||||
# add devices after SIGNAL_DEVICE_SETTED_UP event is listend
|
||||
add_devices(devices)
|
||||
|
||||
def stop_listen(event):
|
||||
"""Stop ZhongHongHub socket."""
|
||||
hub.stop_listen()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_listen)
|
||||
|
||||
|
||||
class ZhongHongClimate(ClimateDevice):
|
||||
"""Representation of a ZhongHong controller support HVAC."""
|
||||
|
||||
def __init__(self, hub, addr_out, addr_in):
|
||||
"""Set up the ZhongHong climate devices."""
|
||||
from zhong_hong_hvac.hvac import HVAC
|
||||
self._device = HVAC(hub, addr_out, addr_in)
|
||||
self._hub = hub
|
||||
self._current_operation = None
|
||||
self._current_temperature = None
|
||||
self._target_temperature = None
|
||||
self._current_fan_mode = None
|
||||
self._is_on = None
|
||||
self.is_initialized = False
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self._device.register_update_callback(self._after_update)
|
||||
self.is_initialized = True
|
||||
async_dispatcher_send(self.hass, SIGNAL_DEVICE_ADDED)
|
||||
|
||||
def _after_update(self, climate):
|
||||
"""Callback to update state."""
|
||||
_LOGGER.debug("async update ha state")
|
||||
if self._device.current_operation:
|
||||
self._current_operation = self._device.current_operation.lower()
|
||||
if self._device.current_temperature:
|
||||
self._current_temperature = self._device.current_temperature
|
||||
if self._device.current_fan_mode:
|
||||
self._current_fan_mode = self._device.current_fan_mode
|
||||
if self._device.target_temperature:
|
||||
self._target_temperature = self._device.target_temperature
|
||||
self._is_on = self._device.is_on
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the thermostat, if any."""
|
||||
return self.unique_id
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of the HVAC."""
|
||||
return "zhong_hong_hvac_{}_{}".format(self._device.addr_out,
|
||||
self._device.addr_in)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
|
||||
| SUPPORT_OPERATION_MODE | SUPPORT_ON_OFF)
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement used by the platform."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._current_operation
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return [STATE_COOL, STATE_HEAT, STATE_DRY, STATE_FAN_ONLY]
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return 1
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if on."""
|
||||
return self._device.is_on
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self._current_fan_mode
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return self._device.fan_list
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._device.min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._device.max_temp
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn on ac."""
|
||||
return self._device.turn_on()
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off ac."""
|
||||
return self._device.turn_off()
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is not None:
|
||||
self._device.set_temperature(temperature)
|
||||
|
||||
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
|
||||
if operation_mode is not None:
|
||||
self.set_operation_mode(operation_mode)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
self._device.set_operation_mode(operation_mode.upper())
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
self._device.set_fan_mode(fan_mode)
|
||||
@@ -2,48 +2,85 @@
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.helpers.entity_registry import async_get_registry
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['websocket_api']
|
||||
|
||||
WS_TYPE_GET = 'config/entity_registry/get'
|
||||
SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_GET,
|
||||
vol.Required('entity_id'): cv.entity_id
|
||||
})
|
||||
|
||||
WS_TYPE_UPDATE = 'config/entity_registry/update'
|
||||
SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_UPDATE,
|
||||
vol.Required('entity_id'): cv.entity_id,
|
||||
# If passed in, we update value. Passing None will remove old value.
|
||||
vol.Optional('name'): vol.Any(str, None),
|
||||
})
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
"""Enable the Entity Registry views."""
|
||||
hass.http.register_view(ConfigManagerEntityView)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_GET, websocket_get_entity,
|
||||
SCHEMA_WS_GET
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_UPDATE, websocket_update_entity,
|
||||
SCHEMA_WS_UPDATE
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
class ConfigManagerEntityView(HomeAssistantView):
|
||||
"""View to interact with an entity registry entry."""
|
||||
@callback
|
||||
def websocket_get_entity(hass, connection, msg):
|
||||
"""Handle get entity registry entry command.
|
||||
|
||||
url = '/api/config/entity_registry/{entity_id}'
|
||||
name = 'api:config:entity_registry:entity'
|
||||
|
||||
async def get(self, request, entity_id):
|
||||
"""Get the entity registry settings for an entity."""
|
||||
hass = request.app['hass']
|
||||
Async friendly.
|
||||
"""
|
||||
async def retrieve_entity():
|
||||
"""Get entity from registry."""
|
||||
registry = await async_get_registry(hass)
|
||||
entry = registry.entities.get(entity_id)
|
||||
entry = registry.entities.get(msg['entity_id'])
|
||||
|
||||
if entry is None:
|
||||
return self.json_message('Entry not found', 404)
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], websocket_api.ERR_NOT_FOUND, 'Entity not found'))
|
||||
return
|
||||
|
||||
return self.json(_entry_dict(entry))
|
||||
connection.send_message_outside(websocket_api.result_message(
|
||||
msg['id'], _entry_dict(entry)
|
||||
))
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
# If passed in, we update value. Passing None will remove old value.
|
||||
vol.Optional('name'): vol.Any(str, None),
|
||||
}))
|
||||
async def post(self, request, entity_id, data):
|
||||
"""Update the entity registry settings for an entity."""
|
||||
hass = request.app['hass']
|
||||
hass.async_add_job(retrieve_entity())
|
||||
|
||||
|
||||
@callback
|
||||
def websocket_update_entity(hass, connection, msg):
|
||||
"""Handle get camera thumbnail websocket command.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
async def update_entity():
|
||||
"""Get entity from registry."""
|
||||
registry = await async_get_registry(hass)
|
||||
|
||||
if entity_id not in registry.entities:
|
||||
return self.json_message('Entry not found', 404)
|
||||
if msg['entity_id'] not in registry.entities:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], websocket_api.ERR_NOT_FOUND, 'Entity not found'))
|
||||
return
|
||||
|
||||
entry = registry.async_update_entity(entity_id, **data)
|
||||
return self.json(_entry_dict(entry))
|
||||
entry = registry.async_update_entity(
|
||||
msg['entity_id'], name=msg['name'])
|
||||
connection.send_message_outside(websocket_api.result_message(
|
||||
msg['id'], _entry_dict(entry)
|
||||
))
|
||||
|
||||
hass.async_add_job(update_entity())
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
@@ -25,7 +25,6 @@ VALUE_TO_STATE = {
|
||||
}
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 cover platform."""
|
||||
|
||||
@@ -107,7 +107,6 @@ class KNXCover(CoverDevice):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
await self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@@ -197,7 +196,6 @@ class KNXCover(CoverDevice):
|
||||
@callback
|
||||
def auto_updater_hook(self, now):
|
||||
"""Call for the autoupdater."""
|
||||
# pylint: disable=unused-argument
|
||||
self.async_schedule_update_ha_state()
|
||||
if self.device.position_reached():
|
||||
self.stop_auto_updater()
|
||||
|
||||
@@ -17,7 +17,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['lutron']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Lutron shades."""
|
||||
devs = []
|
||||
|
||||
@@ -18,7 +18,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['lutron_caseta']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Lutron Caseta shades as a cover device."""
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pymyq==0.0.8']
|
||||
REQUIREMENTS = ['pymyq==0.0.11']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the RPi cover platform."""
|
||||
relay_time = config.get(CONF_RELAY_TIME)
|
||||
|
||||
@@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Vera covers."""
|
||||
add_devices(
|
||||
VeraCover(device, hass.data[VERA_CONTROLLER]) for
|
||||
device in hass.data[VERA_DEVICES]['cover'])
|
||||
[VeraCover(device, hass.data[VERA_CONTROLLER]) for
|
||||
device in hass.data[VERA_DEVICES]['cover']], True)
|
||||
|
||||
|
||||
class VeraCover(VeraDevice, CoverDevice):
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"options": {
|
||||
"title": "Extra configuration options for deCONZ",
|
||||
"data": {
|
||||
"allow_clip_sensor": "Allow importing virtual sensors"
|
||||
"allow_clip_sensor": "Allow importing virtual sensors",
|
||||
"allow_deconz_groups": "Allow importing deCONZ groups"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,7 +8,9 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.util.json import load_json
|
||||
|
||||
from .const import CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN
|
||||
from .const import (
|
||||
CONF_ALLOW_DECONZ_GROUPS, CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN)
|
||||
|
||||
|
||||
CONF_BRIDGEID = 'bridgeid'
|
||||
|
||||
@@ -94,12 +96,15 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
|
||||
"""Extra options for deCONZ.
|
||||
|
||||
CONF_CLIP_SENSOR -- Allow user to choose if they want clip sensors.
|
||||
CONF_DECONZ_GROUPS -- Allow user to choose if they want deCONZ groups.
|
||||
"""
|
||||
from pydeconz.utils import async_get_bridgeid
|
||||
|
||||
if user_input is not None:
|
||||
self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = \
|
||||
user_input[CONF_ALLOW_CLIP_SENSOR]
|
||||
self.deconz_config[CONF_ALLOW_DECONZ_GROUPS] = \
|
||||
user_input[CONF_ALLOW_DECONZ_GROUPS]
|
||||
|
||||
if CONF_BRIDGEID not in self.deconz_config:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
@@ -115,6 +120,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
|
||||
step_id='options',
|
||||
data_schema=vol.Schema({
|
||||
vol.Optional(CONF_ALLOW_CLIP_SENSOR): bool,
|
||||
vol.Optional(CONF_ALLOW_DECONZ_GROUPS): bool,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -158,6 +164,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
|
||||
return await self.async_step_link()
|
||||
|
||||
self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True
|
||||
self.deconz_config[CONF_ALLOW_DECONZ_GROUPS] = True
|
||||
return self.async_create_entry(
|
||||
title='deCONZ-' + self.deconz_config[CONF_BRIDGEID],
|
||||
data=self.deconz_config
|
||||
|
||||
@@ -10,3 +10,4 @@ DATA_DECONZ_ID = 'deconz_entities'
|
||||
DATA_DECONZ_UNSUB = 'deconz_dispatchers'
|
||||
|
||||
CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor'
|
||||
CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"options": {
|
||||
"title": "Extra configuration options for deCONZ",
|
||||
"data":{
|
||||
"allow_clip_sensor": "Allow importing virtual sensors"
|
||||
"allow_clip_sensor": "Allow importing virtual sensors",
|
||||
"allow_deconz_groups": "Allow importing deCONZ groups"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -31,7 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return an Actiontec scanner."""
|
||||
scanner = ActiontecDeviceScanner(config[DOMAIN])
|
||||
|
||||
@@ -30,7 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a Aruba scanner."""
|
||||
scanner = ArubaDeviceScanner(config[DOMAIN])
|
||||
|
||||
@@ -78,7 +78,6 @@ _ARP_REGEX = re.compile(
|
||||
r'.*')
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return an ASUS-WRT scanner."""
|
||||
scanner = AsusWrtDeviceScanner(config[DOMAIN])
|
||||
|
||||
@@ -26,7 +26,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Return a BT Home Hub 5 scanner if successful."""
|
||||
scanner = BTHomeHub5DeviceScanner(config[DOMAIN])
|
||||
|
||||
@@ -27,7 +27,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a DD-WRT scanner."""
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Support for device tracking through Freebox routers.
|
||||
|
||||
This tracker keeps track of the devices connected to the configured Freebox.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.freebox/
|
||||
"""
|
||||
import asyncio
|
||||
import copy
|
||||
import logging
|
||||
import socket
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PORT)
|
||||
|
||||
REQUIREMENTS = ['aiofreepybox==0.0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FREEBOX_CONFIG_FILE = 'freebox.conf'
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PORT): cv.port
|
||||
}))
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
|
||||
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Set up the Freebox device tracker and start the polling."""
|
||||
freebox_config = copy.deepcopy(config)
|
||||
if discovery_info is not None:
|
||||
freebox_config[CONF_HOST] = discovery_info['properties']['api_domain']
|
||||
freebox_config[CONF_PORT] = discovery_info['properties']['https_port']
|
||||
_LOGGER.info("Discovered Freebox server: %s:%s",
|
||||
freebox_config[CONF_HOST], freebox_config[CONF_PORT])
|
||||
|
||||
scanner = FreeboxDeviceScanner(hass, freebox_config, async_see)
|
||||
interval = freebox_config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
await scanner.async_start(hass, interval)
|
||||
return True
|
||||
|
||||
|
||||
Device = namedtuple('Device', ['id', 'name', 'ip'])
|
||||
|
||||
|
||||
def _build_device(device_dict):
|
||||
return Device(
|
||||
device_dict['l2ident']['id'],
|
||||
device_dict['primary_name'],
|
||||
device_dict['l3connectivities'][0]['addr'])
|
||||
|
||||
|
||||
class FreeboxDeviceScanner(object):
|
||||
"""This class scans for devices connected to the Freebox."""
|
||||
|
||||
def __init__(self, hass, config, async_see):
|
||||
"""Initialize the scanner."""
|
||||
from aiofreepybox import Freepybox
|
||||
|
||||
self.host = config[CONF_HOST]
|
||||
self.port = config[CONF_PORT]
|
||||
self.token_file = hass.config.path(FREEBOX_CONFIG_FILE)
|
||||
self.async_see = async_see
|
||||
|
||||
# Hardcode the app description to avoid invalidating the authentication
|
||||
# file at each new version.
|
||||
# The version can be changed if we want the user to re-authorize HASS
|
||||
# on her Freebox.
|
||||
app_desc = {
|
||||
'app_id': 'hass',
|
||||
'app_name': 'Home Assistant',
|
||||
'app_version': '0.65',
|
||||
'device_name': socket.gethostname()
|
||||
}
|
||||
|
||||
api_version = 'v1' # Use the lowest working version.
|
||||
self.fbx = Freepybox(
|
||||
app_desc=app_desc,
|
||||
token_file=self.token_file,
|
||||
api_version=api_version)
|
||||
|
||||
async def async_start(self, hass, interval):
|
||||
"""Perform a first update and start polling at the given interval."""
|
||||
await self.async_update_info()
|
||||
interval = max(interval, MIN_TIME_BETWEEN_SCANS)
|
||||
async_track_time_interval(hass, self.async_update_info, interval)
|
||||
|
||||
async def async_update_info(self, now=None):
|
||||
"""Check the Freebox for devices."""
|
||||
from aiofreepybox.exceptions import HttpRequestError
|
||||
|
||||
_LOGGER.info('Scanning devices')
|
||||
|
||||
await self.fbx.open(self.host, self.port)
|
||||
try:
|
||||
hosts = await self.fbx.lan.get_hosts_list()
|
||||
except HttpRequestError:
|
||||
_LOGGER.exception('Failed to scan devices')
|
||||
else:
|
||||
active_devices = [_build_device(device)
|
||||
for device in hosts
|
||||
if device['active']]
|
||||
|
||||
if active_devices:
|
||||
await asyncio.wait([self.async_see(mac=d.id, host_name=d.name)
|
||||
for d in active_devices])
|
||||
|
||||
await self.fbx.close()
|
||||
@@ -26,7 +26,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a HUAWEI scanner."""
|
||||
scanner = HuaweiDeviceScanner(config[DOMAIN])
|
||||
|
||||
@@ -23,7 +23,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Return a Sky Hub scanner if successful."""
|
||||
scanner = SkyHubDeviceScanner(config[DOMAIN])
|
||||
|
||||
@@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return an SNMP scanner."""
|
||||
scanner = SnmpScanner(config[DOMAIN])
|
||||
|
||||
@@ -33,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a THOMSON scanner."""
|
||||
scanner = ThomsonDeviceScanner(config[DOMAIN])
|
||||
|
||||
@@ -33,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a Unifi direct scanner."""
|
||||
scanner = UnifiDeviceScanner(config[DOMAIN])
|
||||
|
||||
@@ -20,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
|
||||
REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
|
||||
@@ -46,7 +46,9 @@ SERVICE_HOMEKIT = 'homekit'
|
||||
|
||||
CONFIG_ENTRY_HANDLERS = {
|
||||
SERVICE_DECONZ: 'deconz',
|
||||
'google_cast': 'cast',
|
||||
SERVICE_HUE: 'hue',
|
||||
'sonos': 'sonos',
|
||||
}
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
@@ -64,11 +66,9 @@ SERVICE_HANDLERS = {
|
||||
SERVICE_SABNZBD: ('sabnzbd', None),
|
||||
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
|
||||
SERVICE_KONNECTED: ('konnected', None),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
'plex_mediaserver': ('media_player', 'plex'),
|
||||
'roku': ('media_player', 'roku'),
|
||||
'sonos': ('media_player', 'sonos'),
|
||||
'yamaha': ('media_player', 'yamaha'),
|
||||
'logitech_mediaserver': ('media_player', 'squeezebox'),
|
||||
'directv': ('media_player', 'directv'),
|
||||
@@ -84,6 +84,7 @@ SERVICE_HANDLERS = {
|
||||
'kodi': ('media_player', 'kodi'),
|
||||
'volumio': ('media_player', 'volumio'),
|
||||
'nanoleaf_aurora': ('light', 'nanoleaf_aurora'),
|
||||
'freebox': ('device_tracker', 'freebox'),
|
||||
}
|
||||
|
||||
OPTIONAL_SERVICE_HANDLERS = {
|
||||
|
||||
@@ -4,14 +4,16 @@ Support for DoorBird device.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/doorbird/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, \
|
||||
CONF_PASSWORD, CONF_NAME, CONF_DEVICES, CONF_MONITORED_CONDITIONS
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import slugify
|
||||
|
||||
REQUIREMENTS = ['DoorBirdPy==0.1.3']
|
||||
|
||||
@@ -24,60 +26,139 @@ API_URL = '/api/{}'.format(DOMAIN)
|
||||
CONF_DOORBELL_EVENTS = 'doorbell_events'
|
||||
CONF_CUSTOM_URL = 'hass_url_override'
|
||||
|
||||
DOORBELL_EVENT = 'doorbell'
|
||||
MOTION_EVENT = 'motionsensor'
|
||||
|
||||
# Sensor types: Name, device_class, event
|
||||
SENSOR_TYPES = {
|
||||
'doorbell': ['Button', 'occupancy', DOORBELL_EVENT],
|
||||
'motion': ['Motion', 'motion', MOTION_EVENT],
|
||||
}
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_CUSTOM_URL): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=[]):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean,
|
||||
vol.Optional(CONF_CUSTOM_URL): cv.string,
|
||||
})
|
||||
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA])
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SENSOR_DOORBELL = 'doorbell'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the DoorBird component."""
|
||||
from doorbirdpy import DoorBird
|
||||
|
||||
device_ip = config[DOMAIN].get(CONF_HOST)
|
||||
username = config[DOMAIN].get(CONF_USERNAME)
|
||||
password = config[DOMAIN].get(CONF_PASSWORD)
|
||||
# Provide an endpoint for the doorstations to call to trigger events
|
||||
hass.http.register_view(DoorbirdRequestView())
|
||||
|
||||
device = DoorBird(device_ip, username, password)
|
||||
status = device.ready()
|
||||
doorstations = []
|
||||
|
||||
if status[0]:
|
||||
_LOGGER.info("Connected to DoorBird at %s as %s", device_ip, username)
|
||||
hass.data[DOMAIN] = device
|
||||
elif status[1] == 401:
|
||||
_LOGGER.error("Authorization rejected by DoorBird at %s", device_ip)
|
||||
return False
|
||||
else:
|
||||
_LOGGER.error("Could not connect to DoorBird at %s: Error %s",
|
||||
device_ip, str(status[1]))
|
||||
return False
|
||||
for index, doorstation_config in enumerate(config[DOMAIN][CONF_DEVICES]):
|
||||
device_ip = doorstation_config.get(CONF_HOST)
|
||||
username = doorstation_config.get(CONF_USERNAME)
|
||||
password = doorstation_config.get(CONF_PASSWORD)
|
||||
custom_url = doorstation_config.get(CONF_CUSTOM_URL)
|
||||
events = doorstation_config.get(CONF_MONITORED_CONDITIONS)
|
||||
name = (doorstation_config.get(CONF_NAME)
|
||||
or 'DoorBird {}'.format(index + 1))
|
||||
|
||||
if config[DOMAIN].get(CONF_DOORBELL_EVENTS):
|
||||
# Provide an endpoint for the device to call to trigger events
|
||||
hass.http.register_view(DoorbirdRequestView())
|
||||
device = DoorBird(device_ip, username, password)
|
||||
status = device.ready()
|
||||
|
||||
if status[0]:
|
||||
_LOGGER.info("Connected to DoorBird at %s as %s", device_ip,
|
||||
username)
|
||||
doorstation = ConfiguredDoorbird(device, name, events, custom_url)
|
||||
doorstations.append(doorstation)
|
||||
elif status[1] == 401:
|
||||
_LOGGER.error("Authorization rejected by DoorBird at %s",
|
||||
device_ip)
|
||||
return False
|
||||
else:
|
||||
_LOGGER.error("Could not connect to DoorBird at %s: Error %s",
|
||||
device_ip, str(status[1]))
|
||||
return False
|
||||
|
||||
# SETUP EVENT SUBSCRIBERS
|
||||
if events is not None:
|
||||
# This will make HA the only service that receives events.
|
||||
doorstation.device.reset_notifications()
|
||||
|
||||
# Subscribe to doorbell or motion events
|
||||
subscribe_events(hass, doorstation)
|
||||
|
||||
hass.data[DOMAIN] = doorstations
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def subscribe_events(hass, doorstation):
|
||||
"""Initialize the subscriber."""
|
||||
for sensor_type in doorstation.monitored_events:
|
||||
name = '{} {}'.format(doorstation.name,
|
||||
SENSOR_TYPES[sensor_type][0])
|
||||
event_type = SENSOR_TYPES[sensor_type][2]
|
||||
|
||||
# Get the URL of this server
|
||||
hass_url = hass.config.api.base_url
|
||||
|
||||
# Override it if another is specified in the component configuration
|
||||
if config[DOMAIN].get(CONF_CUSTOM_URL):
|
||||
hass_url = config[DOMAIN].get(CONF_CUSTOM_URL)
|
||||
_LOGGER.info("DoorBird will connect to this instance via %s",
|
||||
hass_url)
|
||||
# Override url if another is specified onth configuration
|
||||
if doorstation.custom_url is not None:
|
||||
hass_url = doorstation.custom_url
|
||||
|
||||
# This will make HA the only service that gets doorbell events
|
||||
url = '{}{}/{}'.format(hass_url, API_URL, SENSOR_DOORBELL)
|
||||
device.reset_notifications()
|
||||
device.subscribe_notification(SENSOR_DOORBELL, url)
|
||||
slug = slugify(name)
|
||||
|
||||
return True
|
||||
url = '{}{}/{}'.format(hass_url, API_URL, slug)
|
||||
|
||||
_LOGGER.info("DoorBird will connect to this instance via %s",
|
||||
url)
|
||||
|
||||
_LOGGER.info("You may use the following event name for automations"
|
||||
": %s_%s", DOMAIN, slug)
|
||||
|
||||
doorstation.device.subscribe_notification(event_type, url)
|
||||
|
||||
|
||||
class ConfiguredDoorbird():
|
||||
"""Attach additional information to pass along with configured device."""
|
||||
|
||||
def __init__(self, device, name, events=None, custom_url=None):
|
||||
"""Initialize configured device."""
|
||||
self._name = name
|
||||
self._device = device
|
||||
self._custom_url = custom_url
|
||||
self._monitored_events = events
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Custom device name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
"""The configured device."""
|
||||
return self._device
|
||||
|
||||
@property
|
||||
def custom_url(self):
|
||||
"""Custom url for device."""
|
||||
return self._custom_url
|
||||
|
||||
@property
|
||||
def monitored_events(self):
|
||||
"""Get monitored events."""
|
||||
if self._monitored_events is None:
|
||||
return []
|
||||
|
||||
return self._monitored_events
|
||||
|
||||
|
||||
class DoorbirdRequestView(HomeAssistantView):
|
||||
@@ -93,5 +174,7 @@ class DoorbirdRequestView(HomeAssistantView):
|
||||
def get(self, request, sensor):
|
||||
"""Respond to requests from the device."""
|
||||
hass = request.app['hass']
|
||||
|
||||
hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor))
|
||||
|
||||
return 'OK'
|
||||
|
||||
@@ -48,7 +48,6 @@ def request_configuration(network, hass, config):
|
||||
|
||||
return
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def ecobee_configuration_callback(callback_data):
|
||||
"""Handle configuration callbacks."""
|
||||
network.request_tokens()
|
||||
@@ -106,7 +105,7 @@ def setup(hass, config):
|
||||
Will automatically load thermostat and sensor components to support
|
||||
devices discovered on the network.
|
||||
"""
|
||||
# pylint: disable=global-statement, import-error
|
||||
# pylint: disable=import-error
|
||||
global NETWORK
|
||||
|
||||
if 'ecobee' in _CONFIGURING:
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for Eight smart mattress covers and mattresses.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/eight_sleep/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
@@ -22,7 +21,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
REQUIREMENTS = ['pyeight==0.0.8']
|
||||
REQUIREMENTS = ['pyeight==0.0.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -86,8 +85,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the Eight Sleep component."""
|
||||
from pyeight.eight import EightSleep
|
||||
|
||||
@@ -107,31 +105,29 @@ def async_setup(hass, config):
|
||||
hass.data[DATA_EIGHT] = eight
|
||||
|
||||
# Authenticate, build sensors
|
||||
success = yield from eight.start()
|
||||
success = await eight.start()
|
||||
if not success:
|
||||
# Authentication failed, cannot continue
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_heat_data(now):
|
||||
async def async_update_heat_data(now):
|
||||
"""Update heat data from eight in HEAT_SCAN_INTERVAL."""
|
||||
yield from eight.update_device_data()
|
||||
await eight.update_device_data()
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT)
|
||||
|
||||
async_track_point_in_utc_time(
|
||||
hass, async_update_heat_data, utcnow() + HEAT_SCAN_INTERVAL)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_user_data(now):
|
||||
async def async_update_user_data(now):
|
||||
"""Update user data from eight in USER_SCAN_INTERVAL."""
|
||||
yield from eight.update_user_data()
|
||||
await eight.update_user_data()
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE_USER)
|
||||
|
||||
async_track_point_in_utc_time(
|
||||
hass, async_update_user_data, utcnow() + USER_SCAN_INTERVAL)
|
||||
|
||||
yield from async_update_heat_data(None)
|
||||
yield from async_update_user_data(None)
|
||||
await async_update_heat_data(None)
|
||||
await async_update_user_data(None)
|
||||
|
||||
# Load sub components
|
||||
sensors = []
|
||||
@@ -157,8 +153,7 @@ def async_setup(hass, config):
|
||||
CONF_BINARY_SENSORS: binary_sensors,
|
||||
}, config))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_service_handler(service):
|
||||
async def async_service_handler(service):
|
||||
"""Handle eight sleep service calls."""
|
||||
params = service.data.copy()
|
||||
|
||||
@@ -170,7 +165,7 @@ def async_setup(hass, config):
|
||||
side = sens.split('_')[1]
|
||||
userid = eight.fetch_userid(side)
|
||||
usrobj = eight.users[userid]
|
||||
yield from usrobj.set_heating_level(target, duration)
|
||||
await usrobj.set_heating_level(target, duration)
|
||||
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT)
|
||||
|
||||
@@ -179,10 +174,9 @@ def async_setup(hass, config):
|
||||
DOMAIN, SERVICE_HEAT_SET, async_service_handler,
|
||||
schema=SERVICE_EIGHT_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def stop_eight(event):
|
||||
async def stop_eight(event):
|
||||
"""Handle stopping eight api session."""
|
||||
yield from eight.stop()
|
||||
await eight.stop()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_eight)
|
||||
|
||||
@@ -196,8 +190,7 @@ class EightSleepUserEntity(Entity):
|
||||
"""Initialize the data object."""
|
||||
self._eight = eight
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Register update dispatcher."""
|
||||
@callback
|
||||
def async_eight_user_update():
|
||||
@@ -220,8 +213,7 @@ class EightSleepHeatEntity(Entity):
|
||||
"""Initialize the data object."""
|
||||
self._eight = eight
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Register update dispatcher."""
|
||||
@callback
|
||||
def async_eight_heat_update():
|
||||
|
||||
@@ -13,7 +13,6 @@ FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION
|
||||
LIMITED_SUPPORT = SUPPORT_SET_SPEED
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Set up the demo fan platform."""
|
||||
add_devices_callback([
|
||||
|
||||
@@ -30,7 +30,6 @@ for key in VALUE_TO_STATE:
|
||||
STATE_TO_VALUE[VALUE_TO_STATE[key]] = key
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 fan platform."""
|
||||
|
||||
@@ -49,7 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
'zhimi.humidifier.ca1']),
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
|
||||
REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41']
|
||||
|
||||
ATTR_MODEL = 'model'
|
||||
|
||||
@@ -314,7 +314,6 @@ SERVICE_TO_METHOD = {
|
||||
}
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the miio fan device from config."""
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/frontend/
|
||||
"""
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -24,14 +23,13 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.yaml import load_yaml
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180608.0b0']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180616.0']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
||||
|
||||
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
|
||||
|
||||
CONF_THEMES = 'themes'
|
||||
CONF_EXTRA_HTML_URL = 'extra_html_url'
|
||||
CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5'
|
||||
@@ -99,9 +97,22 @@ WS_TYPE_GET_PANELS = 'get_panels'
|
||||
SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_GET_PANELS,
|
||||
})
|
||||
WS_TYPE_GET_THEMES = 'frontend/get_themes'
|
||||
SCHEMA_GET_THEMES = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_GET_THEMES,
|
||||
})
|
||||
WS_TYPE_GET_TRANSLATIONS = 'frontend/get_translations'
|
||||
SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_GET_TRANSLATIONS,
|
||||
vol.Required('language'): str,
|
||||
})
|
||||
WS_TYPE_GET_EXPERIMENTAL_UI = 'frontend/experimental_ui'
|
||||
SCHEMA_GET_EXPERIMENTAL_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_GET_EXPERIMENTAL_UI,
|
||||
})
|
||||
|
||||
|
||||
class AbstractPanel:
|
||||
class Panel:
|
||||
"""Abstract class for panels."""
|
||||
|
||||
# Name of the webcomponent
|
||||
@@ -113,44 +124,12 @@ class AbstractPanel:
|
||||
# Title to show in the sidebar (optional)
|
||||
sidebar_title = None
|
||||
|
||||
# Url to the webcomponent (depending on JS version)
|
||||
webcomponent_url_es5 = None
|
||||
webcomponent_url_latest = None
|
||||
|
||||
# Url to show the panel in the frontend
|
||||
frontend_url_path = None
|
||||
|
||||
# Config to pass to the webcomponent
|
||||
config = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_register(self, hass):
|
||||
"""Register panel with HASS."""
|
||||
panels = hass.data.get(DATA_PANELS)
|
||||
if panels is None:
|
||||
panels = hass.data[DATA_PANELS] = {}
|
||||
|
||||
if self.frontend_url_path in panels:
|
||||
_LOGGER.warning("Overwriting component %s", self.frontend_url_path)
|
||||
|
||||
if DATA_FINALIZE_PANEL in hass.data:
|
||||
yield from hass.data[DATA_FINALIZE_PANEL](self)
|
||||
|
||||
panels[self.frontend_url_path] = self
|
||||
|
||||
@callback
|
||||
def async_register_index_routes(self, router, index_view):
|
||||
"""Register routes for panel to be served by index view."""
|
||||
router.add_route(
|
||||
'get', '/{}'.format(self.frontend_url_path), index_view.get)
|
||||
router.add_route(
|
||||
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
|
||||
index_view.get)
|
||||
|
||||
|
||||
class BuiltInPanel(AbstractPanel):
|
||||
"""Panel that is part of hass_frontend."""
|
||||
|
||||
def __init__(self, component_name, sidebar_title, sidebar_icon,
|
||||
frontend_url_path, config):
|
||||
"""Initialize a built-in panel."""
|
||||
@@ -160,6 +139,16 @@ class BuiltInPanel(AbstractPanel):
|
||||
self.frontend_url_path = frontend_url_path or component_name
|
||||
self.config = config
|
||||
|
||||
@callback
|
||||
def async_register_index_routes(self, router, index_view):
|
||||
"""Register routes for panel to be served by index view."""
|
||||
router.add_route(
|
||||
'get', '/{}'.format(self.frontend_url_path), index_view.get)
|
||||
router.add_route(
|
||||
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
|
||||
index_view.get)
|
||||
|
||||
@callback
|
||||
def to_response(self, hass, request):
|
||||
"""Panel as dictionary."""
|
||||
return {
|
||||
@@ -171,95 +160,25 @@ class BuiltInPanel(AbstractPanel):
|
||||
}
|
||||
|
||||
|
||||
class ExternalPanel(AbstractPanel):
|
||||
"""Panel that is added by a custom component."""
|
||||
|
||||
REGISTERED_COMPONENTS = set()
|
||||
|
||||
def __init__(self, component_name, path, md5, sidebar_title, sidebar_icon,
|
||||
frontend_url_path, config):
|
||||
"""Initialize an external panel."""
|
||||
self.component_name = component_name
|
||||
self.path = path
|
||||
self.md5 = md5
|
||||
self.sidebar_title = sidebar_title
|
||||
self.sidebar_icon = sidebar_icon
|
||||
self.frontend_url_path = frontend_url_path or component_name
|
||||
self.config = config
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_finalize(self, hass, frontend_repository_path):
|
||||
"""Finalize this panel for usage.
|
||||
|
||||
frontend_repository_path is set, will be prepended to path of built-in
|
||||
components.
|
||||
"""
|
||||
try:
|
||||
if self.md5 is None:
|
||||
self.md5 = yield from hass.async_add_job(
|
||||
_fingerprint, self.path)
|
||||
except OSError:
|
||||
_LOGGER.error('Cannot find or access %s at %s',
|
||||
self.component_name, self.path)
|
||||
hass.data[DATA_PANELS].pop(self.frontend_url_path)
|
||||
return
|
||||
|
||||
self.webcomponent_url_es5 = self.webcomponent_url_latest = \
|
||||
URL_PANEL_COMPONENT_FP.format(self.component_name, self.md5)
|
||||
|
||||
if self.component_name not in self.REGISTERED_COMPONENTS:
|
||||
hass.http.register_static_path(
|
||||
self.webcomponent_url_latest, self.path,
|
||||
# if path is None, we're in prod mode, so cache static assets
|
||||
frontend_repository_path is None)
|
||||
self.REGISTERED_COMPONENTS.add(self.component_name)
|
||||
|
||||
def to_response(self, hass, request):
|
||||
"""Panel as dictionary."""
|
||||
result = {
|
||||
'component_name': self.component_name,
|
||||
'icon': self.sidebar_icon,
|
||||
'title': self.sidebar_title,
|
||||
'url_path': self.frontend_url_path,
|
||||
'config': self.config,
|
||||
}
|
||||
if _is_latest(hass.data[DATA_JS_VERSION], request):
|
||||
result['url'] = self.webcomponent_url_latest
|
||||
else:
|
||||
result['url'] = self.webcomponent_url_es5
|
||||
return result
|
||||
|
||||
|
||||
@bind_hass
|
||||
@asyncio.coroutine
|
||||
def async_register_built_in_panel(hass, component_name, sidebar_title=None,
|
||||
sidebar_icon=None, frontend_url_path=None,
|
||||
config=None):
|
||||
async def async_register_built_in_panel(hass, component_name,
|
||||
sidebar_title=None, sidebar_icon=None,
|
||||
frontend_url_path=None, config=None):
|
||||
"""Register a built-in panel."""
|
||||
panel = BuiltInPanel(component_name, sidebar_title, sidebar_icon,
|
||||
frontend_url_path, config)
|
||||
yield from panel.async_register(hass)
|
||||
panel = Panel(component_name, sidebar_title, sidebar_icon,
|
||||
frontend_url_path, config)
|
||||
|
||||
panels = hass.data.get(DATA_PANELS)
|
||||
if panels is None:
|
||||
panels = hass.data[DATA_PANELS] = {}
|
||||
|
||||
@bind_hass
|
||||
@asyncio.coroutine
|
||||
def async_register_panel(hass, component_name, path, md5=None,
|
||||
sidebar_title=None, sidebar_icon=None,
|
||||
frontend_url_path=None, config=None):
|
||||
"""Register a panel for the frontend.
|
||||
if panel.frontend_url_path in panels:
|
||||
_LOGGER.warning("Overwriting component %s", panel.frontend_url_path)
|
||||
|
||||
component_name: name of the web component
|
||||
path: path to the HTML of the web component
|
||||
(required unless url is provided)
|
||||
md5: the md5 hash of the web component (for versioning in URL, optional)
|
||||
sidebar_title: title to show in the sidebar (optional)
|
||||
sidebar_icon: icon to show next to title in sidebar (optional)
|
||||
url_path: name to use in the URL (defaults to component_name)
|
||||
config: config to be passed into the web component
|
||||
"""
|
||||
panel = ExternalPanel(component_name, path, md5, sidebar_title,
|
||||
sidebar_icon, frontend_url_path, config)
|
||||
yield from panel.async_register(hass)
|
||||
if DATA_FINALIZE_PANEL in hass.data:
|
||||
hass.data[DATA_FINALIZE_PANEL](panel)
|
||||
|
||||
panels[panel.frontend_url_path] = panel
|
||||
|
||||
|
||||
@bind_hass
|
||||
@@ -278,11 +197,10 @@ def add_manifest_json_key(key, val):
|
||||
MANIFEST_JSON[key] = val
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the serving of the frontend."""
|
||||
if list(hass.auth.async_auth_providers):
|
||||
client = yield from hass.auth.async_create_client(
|
||||
client = await hass.auth.async_create_client(
|
||||
'Home Assistant Frontend',
|
||||
redirect_uris=['/'],
|
||||
no_secret=True,
|
||||
@@ -291,7 +209,15 @@ def async_setup(hass, config):
|
||||
client = None
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS)
|
||||
WS_TYPE_GET_PANELS, websocket_get_panels, SCHEMA_GET_PANELS)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_GET_THEMES, websocket_get_themes, SCHEMA_GET_THEMES)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_GET_TRANSLATIONS, websocket_get_translations,
|
||||
SCHEMA_GET_TRANSLATIONS)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_GET_EXPERIMENTAL_UI, websocket_experimental_config,
|
||||
SCHEMA_GET_EXPERIMENTAL_UI)
|
||||
hass.http.register_view(ManifestJSONView)
|
||||
|
||||
conf = config.get(DOMAIN, {})
|
||||
@@ -331,24 +257,23 @@ def async_setup(hass, config):
|
||||
index_view = IndexView(repo_path, js_version, client)
|
||||
hass.http.register_view(index_view)
|
||||
|
||||
async def finalize_panel(panel):
|
||||
@callback
|
||||
def async_finalize_panel(panel):
|
||||
"""Finalize setup of a panel."""
|
||||
if hasattr(panel, 'async_finalize'):
|
||||
await panel.async_finalize(hass, repo_path)
|
||||
panel.async_register_index_routes(hass.http.app.router, index_view)
|
||||
|
||||
yield from asyncio.wait([
|
||||
async_register_built_in_panel(hass, panel)
|
||||
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
|
||||
'dev-template', 'dev-mqtt', 'kiosk')], loop=hass.loop)
|
||||
await asyncio.wait(
|
||||
[async_register_built_in_panel(hass, panel) for panel in (
|
||||
'dev-event', 'dev-info', 'dev-service', 'dev-state',
|
||||
'dev-template', 'dev-mqtt', 'kiosk', 'experimental-ui')],
|
||||
loop=hass.loop)
|
||||
|
||||
hass.data[DATA_FINALIZE_PANEL] = finalize_panel
|
||||
hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel
|
||||
|
||||
# Finalize registration of panels that registered before frontend was setup
|
||||
# This includes the built-in panels from line above.
|
||||
yield from asyncio.wait(
|
||||
[finalize_panel(panel) for panel in hass.data[DATA_PANELS].values()],
|
||||
loop=hass.loop)
|
||||
for panel in hass.data[DATA_PANELS].values():
|
||||
async_finalize_panel(panel)
|
||||
|
||||
if DATA_EXTRA_HTML_URL not in hass.data:
|
||||
hass.data[DATA_EXTRA_HTML_URL] = set()
|
||||
@@ -360,16 +285,14 @@ def async_setup(hass, config):
|
||||
for url in conf.get(CONF_EXTRA_HTML_URL_ES5, []):
|
||||
add_extra_html_url(hass, url, True)
|
||||
|
||||
async_setup_themes(hass, conf.get(CONF_THEMES))
|
||||
|
||||
hass.http.register_view(TranslationsView)
|
||||
_async_setup_themes(hass, conf.get(CONF_THEMES))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def async_setup_themes(hass, themes):
|
||||
@callback
|
||||
def _async_setup_themes(hass, themes):
|
||||
"""Set up themes data and services."""
|
||||
hass.http.register_view(ThemesView)
|
||||
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
|
||||
if themes is None:
|
||||
hass.data[DATA_THEMES] = {}
|
||||
@@ -456,38 +379,23 @@ class IndexView(HomeAssistantView):
|
||||
|
||||
return tpl
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, extra=None):
|
||||
async def get(self, request, extra=None):
|
||||
"""Serve the index view."""
|
||||
hass = request.app['hass']
|
||||
latest = self.repo_path is not None or \
|
||||
_is_latest(self.js_option, request)
|
||||
|
||||
if request.path == '/':
|
||||
panel = 'states'
|
||||
else:
|
||||
panel = request.path.split('/')[1]
|
||||
|
||||
if panel == 'states':
|
||||
panel_url = ''
|
||||
elif latest:
|
||||
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_latest
|
||||
else:
|
||||
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_es5
|
||||
|
||||
no_auth = '1'
|
||||
if hass.config.api.api_password and not request[KEY_AUTHENTICATED]:
|
||||
# do not try to auto connect on load
|
||||
no_auth = '0'
|
||||
|
||||
template = yield from hass.async_add_job(self.get_template, latest)
|
||||
template = await hass.async_add_job(self.get_template, latest)
|
||||
|
||||
extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5
|
||||
|
||||
template_params = dict(
|
||||
no_auth=no_auth,
|
||||
panel_url=panel_url,
|
||||
panels=hass.data[DATA_PANELS],
|
||||
theme_color=MANIFEST_JSON['theme_color'],
|
||||
extra_urls=hass.data[extra_key],
|
||||
)
|
||||
@@ -506,54 +414,13 @@ class ManifestJSONView(HomeAssistantView):
|
||||
url = '/manifest.json'
|
||||
name = 'manifestjson'
|
||||
|
||||
@asyncio.coroutine
|
||||
@callback
|
||||
def get(self, request): # pylint: disable=no-self-use
|
||||
"""Return the manifest.json."""
|
||||
msg = json.dumps(MANIFEST_JSON, sort_keys=True)
|
||||
return web.Response(text=msg, content_type="application/manifest+json")
|
||||
|
||||
|
||||
class ThemesView(HomeAssistantView):
|
||||
"""View to return defined themes."""
|
||||
|
||||
requires_auth = False
|
||||
url = '/api/themes'
|
||||
name = 'api:themes'
|
||||
|
||||
@callback
|
||||
def get(self, request):
|
||||
"""Return themes."""
|
||||
hass = request.app['hass']
|
||||
|
||||
return self.json({
|
||||
'themes': hass.data[DATA_THEMES],
|
||||
'default_theme': hass.data[DATA_DEFAULT_THEME],
|
||||
})
|
||||
|
||||
|
||||
class TranslationsView(HomeAssistantView):
|
||||
"""View to return backend defined translations."""
|
||||
|
||||
url = '/api/translations/{language}'
|
||||
name = 'api:translations'
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, language):
|
||||
"""Return translations."""
|
||||
hass = request.app['hass']
|
||||
|
||||
resources = yield from async_get_translations(hass, language)
|
||||
return self.json({
|
||||
'resources': resources,
|
||||
})
|
||||
|
||||
|
||||
def _fingerprint(path):
|
||||
"""Fingerprint a file."""
|
||||
with open(path) as fil:
|
||||
return hashlib.md5(fil.read().encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def _is_latest(js_option, request):
|
||||
"""
|
||||
Return whether we should serve latest untranspiled code.
|
||||
@@ -587,7 +454,7 @@ def _is_latest(js_option, request):
|
||||
|
||||
|
||||
@callback
|
||||
def websocket_handle_get_panels(hass, connection, msg):
|
||||
def websocket_get_panels(hass, connection, msg):
|
||||
"""Handle get panels command.
|
||||
|
||||
Async friendly.
|
||||
@@ -600,3 +467,47 @@ def websocket_handle_get_panels(hass, connection, msg):
|
||||
|
||||
connection.to_write.put_nowait(websocket_api.result_message(
|
||||
msg['id'], panels))
|
||||
|
||||
|
||||
@callback
|
||||
def websocket_get_themes(hass, connection, msg):
|
||||
"""Handle get themes command.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
connection.to_write.put_nowait(websocket_api.result_message(msg['id'], {
|
||||
'themes': hass.data[DATA_THEMES],
|
||||
'default_theme': hass.data[DATA_DEFAULT_THEME],
|
||||
}))
|
||||
|
||||
|
||||
@callback
|
||||
def websocket_get_translations(hass, connection, msg):
|
||||
"""Handle get translations command.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
async def send_translations():
|
||||
"""Send a camera still."""
|
||||
resources = await async_get_translations(hass, msg['language'])
|
||||
connection.send_message_outside(websocket_api.result_message(
|
||||
msg['id'], {
|
||||
'resources': resources,
|
||||
}
|
||||
))
|
||||
|
||||
hass.async_add_job(send_translations())
|
||||
|
||||
|
||||
def websocket_experimental_config(hass, connection, msg):
|
||||
"""Send experimental UI config over websocket config."""
|
||||
async def send_exp_config():
|
||||
"""Send experimental frontend config."""
|
||||
config = await hass.async_add_job(
|
||||
load_yaml, hass.config.path('experimental-ui.yaml'))
|
||||
|
||||
connection.send_message_outside(websocket_api.result_message(
|
||||
msg['id'], config
|
||||
))
|
||||
|
||||
hass.async_add_job(send_exp_config())
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -171,14 +171,20 @@ def async_setup(hass, config):
|
||||
development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO)
|
||||
if development_repo is not None:
|
||||
hass.http.register_static_path(
|
||||
'/api/hassio/app-es5',
|
||||
os.path.join(development_repo, 'hassio/build-es5'), False)
|
||||
'/api/hassio/app',
|
||||
os.path.join(development_repo, 'hassio/build'), False)
|
||||
|
||||
hass.http.register_view(HassIOView(host, websession))
|
||||
|
||||
if 'frontend' in hass.config.components:
|
||||
yield from hass.components.frontend.async_register_built_in_panel(
|
||||
'hassio', 'Hass.io', 'hass:home-assistant')
|
||||
yield from hass.components.panel_custom.async_register_panel(
|
||||
frontend_url_path='hassio',
|
||||
webcomponent_name='hassio-main',
|
||||
sidebar_title='Hass.io',
|
||||
sidebar_icon='hass:home-assistant',
|
||||
js_url='/api/hassio/app/entrypoint.js',
|
||||
embed_iframe=True,
|
||||
)
|
||||
|
||||
if 'http' in config:
|
||||
yield from hassio.update_hass_api(config['http'])
|
||||
|
||||
@@ -37,6 +37,7 @@ def homekit_http_send(self, message_body=None, encode_chunked=False):
|
||||
Appends an extra \r\n to the buffer.
|
||||
A message_body may be specified, to be appended to the request.
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
self._buffer.extend((b"", b""))
|
||||
msg = b"\r\n".join(self._buffer)
|
||||
del self._buffer[:]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user