mirror of
https://github.com/home-assistant/core.git
synced 2025-07-29 16:17:20 +00:00
commit
4770888d22
34
.coveragerc
34
.coveragerc
@ -37,6 +37,9 @@ omit =
|
|||||||
homeassistant/components/isy994.py
|
homeassistant/components/isy994.py
|
||||||
homeassistant/components/*/isy994.py
|
homeassistant/components/*/isy994.py
|
||||||
|
|
||||||
|
homeassistant/components/litejet.py
|
||||||
|
homeassistant/components/*/litejet.py
|
||||||
|
|
||||||
homeassistant/components/modbus.py
|
homeassistant/components/modbus.py
|
||||||
homeassistant/components/*/modbus.py
|
homeassistant/components/*/modbus.py
|
||||||
|
|
||||||
@ -98,8 +101,6 @@ omit =
|
|||||||
homeassistant/components/homematic.py
|
homeassistant/components/homematic.py
|
||||||
homeassistant/components/*/homematic.py
|
homeassistant/components/*/homematic.py
|
||||||
|
|
||||||
homeassistant/components/switch/pilight.py
|
|
||||||
|
|
||||||
homeassistant/components/knx.py
|
homeassistant/components/knx.py
|
||||||
homeassistant/components/*/knx.py
|
homeassistant/components/*/knx.py
|
||||||
|
|
||||||
@ -109,6 +110,9 @@ omit =
|
|||||||
homeassistant/components/zoneminder.py
|
homeassistant/components/zoneminder.py
|
||||||
homeassistant/components/*/zoneminder.py
|
homeassistant/components/*/zoneminder.py
|
||||||
|
|
||||||
|
homeassistant/components/mochad.py
|
||||||
|
homeassistant/components/*/mochad.py
|
||||||
|
|
||||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||||
homeassistant/components/alarm_control_panel/concord232.py
|
homeassistant/components/alarm_control_panel/concord232.py
|
||||||
homeassistant/components/alarm_control_panel/nx584.py
|
homeassistant/components/alarm_control_panel/nx584.py
|
||||||
@ -128,6 +132,7 @@ omit =
|
|||||||
homeassistant/components/climate/knx.py
|
homeassistant/components/climate/knx.py
|
||||||
homeassistant/components/climate/proliphix.py
|
homeassistant/components/climate/proliphix.py
|
||||||
homeassistant/components/climate/radiotherm.py
|
homeassistant/components/climate/radiotherm.py
|
||||||
|
homeassistant/components/cover/garadget.py
|
||||||
homeassistant/components/cover/homematic.py
|
homeassistant/components/cover/homematic.py
|
||||||
homeassistant/components/cover/rpi_gpio.py
|
homeassistant/components/cover/rpi_gpio.py
|
||||||
homeassistant/components/cover/scsgate.py
|
homeassistant/components/cover/scsgate.py
|
||||||
@ -136,10 +141,9 @@ omit =
|
|||||||
homeassistant/components/device_tracker/aruba.py
|
homeassistant/components/device_tracker/aruba.py
|
||||||
homeassistant/components/device_tracker/asuswrt.py
|
homeassistant/components/device_tracker/asuswrt.py
|
||||||
homeassistant/components/device_tracker/bbox.py
|
homeassistant/components/device_tracker/bbox.py
|
||||||
homeassistant/components/device_tracker/bluetooth_tracker.py
|
|
||||||
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
||||||
|
homeassistant/components/device_tracker/bluetooth_tracker.py
|
||||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||||
homeassistant/components/device_tracker/ddwrt.py
|
|
||||||
homeassistant/components/device_tracker/fritz.py
|
homeassistant/components/device_tracker/fritz.py
|
||||||
homeassistant/components/device_tracker/icloud.py
|
homeassistant/components/device_tracker/icloud.py
|
||||||
homeassistant/components/device_tracker/luci.py
|
homeassistant/components/device_tracker/luci.py
|
||||||
@ -157,8 +161,6 @@ omit =
|
|||||||
homeassistant/components/fan/mqtt.py
|
homeassistant/components/fan/mqtt.py
|
||||||
homeassistant/components/feedreader.py
|
homeassistant/components/feedreader.py
|
||||||
homeassistant/components/foursquare.py
|
homeassistant/components/foursquare.py
|
||||||
homeassistant/components/garage_door/rpi_gpio.py
|
|
||||||
homeassistant/components/garage_door/wink.py
|
|
||||||
homeassistant/components/hdmi_cec.py
|
homeassistant/components/hdmi_cec.py
|
||||||
homeassistant/components/ifttt.py
|
homeassistant/components/ifttt.py
|
||||||
homeassistant/components/joaoapps_join.py
|
homeassistant/components/joaoapps_join.py
|
||||||
@ -172,12 +174,14 @@ omit =
|
|||||||
homeassistant/components/light/limitlessled.py
|
homeassistant/components/light/limitlessled.py
|
||||||
homeassistant/components/light/osramlightify.py
|
homeassistant/components/light/osramlightify.py
|
||||||
homeassistant/components/light/x10.py
|
homeassistant/components/light/x10.py
|
||||||
|
homeassistant/components/light/yeelight.py
|
||||||
homeassistant/components/lirc.py
|
homeassistant/components/lirc.py
|
||||||
homeassistant/components/media_player/braviatv.py
|
homeassistant/components/media_player/braviatv.py
|
||||||
homeassistant/components/media_player/cast.py
|
homeassistant/components/media_player/cast.py
|
||||||
homeassistant/components/media_player/cmus.py
|
homeassistant/components/media_player/cmus.py
|
||||||
homeassistant/components/media_player/denon.py
|
homeassistant/components/media_player/denon.py
|
||||||
homeassistant/components/media_player/directv.py
|
homeassistant/components/media_player/directv.py
|
||||||
|
homeassistant/components/media_player/emby.py
|
||||||
homeassistant/components/media_player/firetv.py
|
homeassistant/components/media_player/firetv.py
|
||||||
homeassistant/components/media_player/gpmdp.py
|
homeassistant/components/media_player/gpmdp.py
|
||||||
homeassistant/components/media_player/itunes.py
|
homeassistant/components/media_player/itunes.py
|
||||||
@ -188,6 +192,7 @@ omit =
|
|||||||
homeassistant/components/media_player/onkyo.py
|
homeassistant/components/media_player/onkyo.py
|
||||||
homeassistant/components/media_player/panasonic_viera.py
|
homeassistant/components/media_player/panasonic_viera.py
|
||||||
homeassistant/components/media_player/pandora.py
|
homeassistant/components/media_player/pandora.py
|
||||||
|
homeassistant/components/media_player/philips_js.py
|
||||||
homeassistant/components/media_player/pioneer.py
|
homeassistant/components/media_player/pioneer.py
|
||||||
homeassistant/components/media_player/plex.py
|
homeassistant/components/media_player/plex.py
|
||||||
homeassistant/components/media_player/roku.py
|
homeassistant/components/media_player/roku.py
|
||||||
@ -209,6 +214,7 @@ omit =
|
|||||||
homeassistant/components/notify/llamalab_automate.py
|
homeassistant/components/notify/llamalab_automate.py
|
||||||
homeassistant/components/notify/matrix.py
|
homeassistant/components/notify/matrix.py
|
||||||
homeassistant/components/notify/message_bird.py
|
homeassistant/components/notify/message_bird.py
|
||||||
|
homeassistant/components/notify/nfandroidtv.py
|
||||||
homeassistant/components/notify/nma.py
|
homeassistant/components/notify/nma.py
|
||||||
homeassistant/components/notify/pushbullet.py
|
homeassistant/components/notify/pushbullet.py
|
||||||
homeassistant/components/notify/pushetta.py
|
homeassistant/components/notify/pushetta.py
|
||||||
@ -234,9 +240,12 @@ omit =
|
|||||||
homeassistant/components/sensor/bom.py
|
homeassistant/components/sensor/bom.py
|
||||||
homeassistant/components/sensor/coinmarketcap.py
|
homeassistant/components/sensor/coinmarketcap.py
|
||||||
homeassistant/components/sensor/cpuspeed.py
|
homeassistant/components/sensor/cpuspeed.py
|
||||||
|
homeassistant/components/sensor/cups.py
|
||||||
|
homeassistant/components/sensor/currencylayer.py
|
||||||
homeassistant/components/sensor/darksky.py
|
homeassistant/components/sensor/darksky.py
|
||||||
homeassistant/components/sensor/deutsche_bahn.py
|
homeassistant/components/sensor/deutsche_bahn.py
|
||||||
homeassistant/components/sensor/dht.py
|
homeassistant/components/sensor/dht.py
|
||||||
|
homeassistant/components/sensor/dovado.py
|
||||||
homeassistant/components/sensor/dte_energy_bridge.py
|
homeassistant/components/sensor/dte_energy_bridge.py
|
||||||
homeassistant/components/sensor/efergy.py
|
homeassistant/components/sensor/efergy.py
|
||||||
homeassistant/components/sensor/eliqonline.py
|
homeassistant/components/sensor/eliqonline.py
|
||||||
@ -250,9 +259,11 @@ omit =
|
|||||||
homeassistant/components/sensor/gpsd.py
|
homeassistant/components/sensor/gpsd.py
|
||||||
homeassistant/components/sensor/gtfs.py
|
homeassistant/components/sensor/gtfs.py
|
||||||
homeassistant/components/sensor/haveibeenpwned.py
|
homeassistant/components/sensor/haveibeenpwned.py
|
||||||
|
homeassistant/components/sensor/hddtemp.py
|
||||||
homeassistant/components/sensor/hp_ilo.py
|
homeassistant/components/sensor/hp_ilo.py
|
||||||
homeassistant/components/sensor/imap.py
|
homeassistant/components/sensor/imap.py
|
||||||
homeassistant/components/sensor/imap_email_content.py
|
homeassistant/components/sensor/imap_email_content.py
|
||||||
|
homeassistant/components/sensor/influxdb.py
|
||||||
homeassistant/components/sensor/lastfm.py
|
homeassistant/components/sensor/lastfm.py
|
||||||
homeassistant/components/sensor/linux_battery.py
|
homeassistant/components/sensor/linux_battery.py
|
||||||
homeassistant/components/sensor/loopenergy.py
|
homeassistant/components/sensor/loopenergy.py
|
||||||
@ -267,7 +278,6 @@ omit =
|
|||||||
homeassistant/components/sensor/openweathermap.py
|
homeassistant/components/sensor/openweathermap.py
|
||||||
homeassistant/components/sensor/pi_hole.py
|
homeassistant/components/sensor/pi_hole.py
|
||||||
homeassistant/components/sensor/plex.py
|
homeassistant/components/sensor/plex.py
|
||||||
homeassistant/components/sensor/rest.py
|
|
||||||
homeassistant/components/sensor/sabnzbd.py
|
homeassistant/components/sensor/sabnzbd.py
|
||||||
homeassistant/components/sensor/scrape.py
|
homeassistant/components/sensor/scrape.py
|
||||||
homeassistant/components/sensor/serial_pm.py
|
homeassistant/components/sensor/serial_pm.py
|
||||||
@ -277,6 +287,7 @@ omit =
|
|||||||
homeassistant/components/sensor/supervisord.py
|
homeassistant/components/sensor/supervisord.py
|
||||||
homeassistant/components/sensor/swiss_hydrological_data.py
|
homeassistant/components/sensor/swiss_hydrological_data.py
|
||||||
homeassistant/components/sensor/swiss_public_transport.py
|
homeassistant/components/sensor/swiss_public_transport.py
|
||||||
|
homeassistant/components/sensor/synologydsm.py
|
||||||
homeassistant/components/sensor/systemmonitor.py
|
homeassistant/components/sensor/systemmonitor.py
|
||||||
homeassistant/components/sensor/ted5000.py
|
homeassistant/components/sensor/ted5000.py
|
||||||
homeassistant/components/sensor/temper.py
|
homeassistant/components/sensor/temper.py
|
||||||
@ -286,7 +297,6 @@ omit =
|
|||||||
homeassistant/components/sensor/twitch.py
|
homeassistant/components/sensor/twitch.py
|
||||||
homeassistant/components/sensor/uber.py
|
homeassistant/components/sensor/uber.py
|
||||||
homeassistant/components/sensor/vasttrafik.py
|
homeassistant/components/sensor/vasttrafik.py
|
||||||
homeassistant/components/sensor/worldclock.py
|
|
||||||
homeassistant/components/sensor/xbox_live.py
|
homeassistant/components/sensor/xbox_live.py
|
||||||
homeassistant/components/sensor/yweather.py
|
homeassistant/components/sensor/yweather.py
|
||||||
homeassistant/components/switch/acer_projector.py
|
homeassistant/components/switch/acer_projector.py
|
||||||
@ -299,18 +309,16 @@ omit =
|
|||||||
homeassistant/components/switch/neato.py
|
homeassistant/components/switch/neato.py
|
||||||
homeassistant/components/switch/netio.py
|
homeassistant/components/switch/netio.py
|
||||||
homeassistant/components/switch/orvibo.py
|
homeassistant/components/switch/orvibo.py
|
||||||
|
homeassistant/components/switch/pilight.py
|
||||||
homeassistant/components/switch/pulseaudio_loopback.py
|
homeassistant/components/switch/pulseaudio_loopback.py
|
||||||
homeassistant/components/switch/rest.py
|
homeassistant/components/switch/rest.py
|
||||||
homeassistant/components/switch/rpi_rf.py
|
homeassistant/components/switch/rpi_rf.py
|
||||||
homeassistant/components/switch/tplink.py
|
homeassistant/components/switch/tplink.py
|
||||||
homeassistant/components/switch/transmission.py
|
homeassistant/components/switch/transmission.py
|
||||||
homeassistant/components/switch/wake_on_lan.py
|
homeassistant/components/switch/wake_on_lan.py
|
||||||
homeassistant/components/thermostat/eq3btsmart.py
|
homeassistant/components/thingspeak.py
|
||||||
homeassistant/components/thermostat/heatmiser.py
|
|
||||||
homeassistant/components/thermostat/homematic.py
|
|
||||||
homeassistant/components/thermostat/proliphix.py
|
|
||||||
homeassistant/components/thermostat/radiotherm.py
|
|
||||||
homeassistant/components/upnp.py
|
homeassistant/components/upnp.py
|
||||||
|
homeassistant/components/weather/openweathermap.py
|
||||||
homeassistant/components/zeroconf.py
|
homeassistant/components/zeroconf.py
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,11 +2,11 @@ sudo: false
|
|||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
include:
|
include:
|
||||||
- python: "3.4"
|
- python: "3.4.2"
|
||||||
env: TOXENV=py34
|
env: TOXENV=py34
|
||||||
- python: "3.4"
|
- python: "3.4.2"
|
||||||
env: TOXENV=requirements
|
env: TOXENV=requirements
|
||||||
- python: "3.5"
|
- python: "3.4.2"
|
||||||
env: TOXENV=lint
|
env: TOXENV=lint
|
||||||
- python: "3.5"
|
- python: "3.5"
|
||||||
env: TOXENV=typing
|
env: TOXENV=typing
|
||||||
|
@ -43,12 +43,10 @@ device_tracker:
|
|||||||
username: admin
|
username: admin
|
||||||
password: PASSWORD
|
password: PASSWORD
|
||||||
|
|
||||||
chromecast:
|
|
||||||
|
|
||||||
switch:
|
switch:
|
||||||
platform: wemo
|
platform: wemo
|
||||||
|
|
||||||
thermostat:
|
climate:
|
||||||
platform: nest
|
platform: nest
|
||||||
# Required: username and password that are used to login to the Nest thermostat.
|
# Required: username and password that are used to login to the Nest thermostat.
|
||||||
username: myemail@mydomain.com
|
username: myemail@mydomain.com
|
||||||
@ -79,7 +77,6 @@ group:
|
|||||||
entities:
|
entities:
|
||||||
- group.awesome_people
|
- group.awesome_people
|
||||||
- group.climate
|
- group.climate
|
||||||
|
|
||||||
kitchen:
|
kitchen:
|
||||||
name: Kitchen
|
name: Kitchen
|
||||||
entities:
|
entities:
|
||||||
@ -92,52 +89,23 @@ group:
|
|||||||
- input_boolean.notify_home
|
- input_boolean.notify_home
|
||||||
- camera.demo_camera
|
- camera.demo_camera
|
||||||
|
|
||||||
example:
|
|
||||||
|
|
||||||
simple_alarm:
|
|
||||||
# Which light/light group has to flash when a known device comes home
|
|
||||||
known_light: light.Bowl
|
|
||||||
# Which light/light group has to flash red when light turns on while no one home
|
|
||||||
unknown_light: group.living_room
|
|
||||||
|
|
||||||
browser:
|
browser:
|
||||||
|
|
||||||
keyboard:
|
keyboard:
|
||||||
|
|
||||||
# https://home-assistant.io/getting-started/automation/
|
# https://home-assistant.io/getting-started/automation/
|
||||||
automation:
|
automation:
|
||||||
- alias: 'Rule 1 Light on in the evening'
|
- alias: Turn on light when sun sets
|
||||||
trigger:
|
trigger:
|
||||||
- platform: sun
|
platform: sun
|
||||||
event: sunset
|
event: sunset
|
||||||
offset: "-01:00:00"
|
offset: "-01:00:00"
|
||||||
- platform: state
|
|
||||||
entity_id: group.all_devices
|
|
||||||
state: home
|
|
||||||
condition:
|
condition:
|
||||||
- platform: state
|
condition: state
|
||||||
entity_id: group.all_devices
|
entity_id: group.all_devices
|
||||||
state: home
|
state: 'home'
|
||||||
- platform: time
|
|
||||||
after: "16:00:00"
|
|
||||||
before: "23:00:00"
|
|
||||||
action:
|
action:
|
||||||
service: homeassistant.turn_on
|
service: light.turn_on
|
||||||
entity_id: group.living_room
|
|
||||||
|
|
||||||
- alias: 'Rule 2 - Away Mode'
|
|
||||||
trigger:
|
|
||||||
- platform: state
|
|
||||||
entity_id: group.all_devices
|
|
||||||
state: 'not_home'
|
|
||||||
|
|
||||||
condition: use_trigger_values
|
|
||||||
action:
|
|
||||||
service: light.turn_off
|
|
||||||
entity_id: group.all_lights
|
|
||||||
|
|
||||||
# Sensors need to be added into the configuration.yaml as sensor:, sensor 2:, sensor 3:, etc.
|
|
||||||
# Each sensor label should be unique or your sensors might not load correctly.
|
|
||||||
# Another way to do is to collect all entries under one "sensor:"
|
# Another way to do is to collect all entries under one "sensor:"
|
||||||
# sensor:
|
# sensor:
|
||||||
# - platform: mqtt
|
# - platform: mqtt
|
||||||
@ -154,34 +122,30 @@ sensor:
|
|||||||
arg: '/'
|
arg: '/'
|
||||||
- type: 'disk_use_percent'
|
- type: 'disk_use_percent'
|
||||||
arg: '/home'
|
arg: '/home'
|
||||||
- type: 'disk_use'
|
|
||||||
arg: '/home'
|
|
||||||
|
|
||||||
sensor 2:
|
sensor 2:
|
||||||
platform: forecast
|
platform: cpuspeed
|
||||||
api_key: <register on Forecast.io for your PRIVATE API>
|
|
||||||
monitored_conditions:
|
|
||||||
- summary
|
|
||||||
- precip_type
|
|
||||||
- precip_intensity
|
|
||||||
- temperature
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
# Turns on the bedroom lights and then the living room lights 1 minute later
|
|
||||||
wakeup:
|
wakeup:
|
||||||
alias: Wake Up
|
alias: Wake Up
|
||||||
sequence:
|
sequence:
|
||||||
# alias is optional
|
- event: LOGBOOK_ENTRY
|
||||||
|
event_data:
|
||||||
|
name: Paulus
|
||||||
|
message: is waking up
|
||||||
|
entity_id: device_tracker.paulus
|
||||||
|
domain: light
|
||||||
- alias: Bedroom lights on
|
- alias: Bedroom lights on
|
||||||
execute_service: light.turn_on
|
service: light.turn_on
|
||||||
service_data:
|
data:
|
||||||
entity_id: group.bedroom
|
entity_id: group.bedroom
|
||||||
|
brightness: 100
|
||||||
- delay:
|
- delay:
|
||||||
# supports seconds, milliseconds, minutes, hours, etc.
|
|
||||||
minutes: 1
|
minutes: 1
|
||||||
- alias: Living room lights on
|
- alias: Living room lights on
|
||||||
execute_service: light.turn_on
|
service: light.turn_on
|
||||||
service_data:
|
data:
|
||||||
entity_id: group.living_room
|
entity_id: group.living_room
|
||||||
|
|
||||||
scene:
|
scene:
|
||||||
|
@ -14,6 +14,7 @@ from homeassistant.const import (
|
|||||||
__version__,
|
__version__,
|
||||||
EVENT_HOMEASSISTANT_START,
|
EVENT_HOMEASSISTANT_START,
|
||||||
REQUIRED_PYTHON_VER,
|
REQUIRED_PYTHON_VER,
|
||||||
|
REQUIRED_PYTHON_VER_WIN,
|
||||||
RESTART_EXIT_CODE,
|
RESTART_EXIT_CODE,
|
||||||
)
|
)
|
||||||
from homeassistant.util.async import run_callback_threadsafe
|
from homeassistant.util.async import run_callback_threadsafe
|
||||||
@ -44,8 +45,7 @@ def monkey_patch_asyncio():
|
|||||||
See https://bugs.python.org/issue26617 for details of the Python
|
See https://bugs.python.org/issue26617 for details of the Python
|
||||||
bug.
|
bug.
|
||||||
"""
|
"""
|
||||||
# pylint: disable=no-self-use, too-few-public-methods, protected-access
|
# pylint: disable=no-self-use, protected-access, bare-except
|
||||||
# pylint: disable=bare-except
|
|
||||||
import asyncio.tasks
|
import asyncio.tasks
|
||||||
|
|
||||||
class IgnoreCalls:
|
class IgnoreCalls:
|
||||||
@ -64,7 +64,12 @@ def monkey_patch_asyncio():
|
|||||||
|
|
||||||
def validate_python() -> None:
|
def validate_python() -> None:
|
||||||
"""Validate we're running the right Python version."""
|
"""Validate we're running the right Python version."""
|
||||||
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
if sys.platform == "win32" and \
|
||||||
|
sys.version_info[:3] < REQUIRED_PYTHON_VER_WIN:
|
||||||
|
print("Home Assistant requires at least Python {}.{}.{}".format(
|
||||||
|
*REQUIRED_PYTHON_VER_WIN))
|
||||||
|
sys.exit(1)
|
||||||
|
elif sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
||||||
print("Home Assistant requires at least Python {}.{}.{}".format(
|
print("Home Assistant requires at least Python {}.{}.{}".format(
|
||||||
*REQUIRED_PYTHON_VER))
|
*REQUIRED_PYTHON_VER))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
"""Provides methods to bootstrap a home assistant instance."""
|
"""Provides methods to bootstrap a home assistant instance."""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from threading import RLock
|
|
||||||
|
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any, Optional, Dict
|
from typing import Any, Optional, Dict
|
||||||
@ -19,6 +18,8 @@ import homeassistant.config as conf_util
|
|||||||
import homeassistant.core as core
|
import homeassistant.core as core
|
||||||
import homeassistant.loader as loader
|
import homeassistant.loader as loader
|
||||||
import homeassistant.util.package as pkg_util
|
import homeassistant.util.package as pkg_util
|
||||||
|
from homeassistant.util.async import (
|
||||||
|
run_coroutine_threadsafe, run_callback_threadsafe)
|
||||||
from homeassistant.util.yaml import clear_secret_cache
|
from homeassistant.util.yaml import clear_secret_cache
|
||||||
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
@ -26,24 +27,34 @@ from homeassistant.helpers import (
|
|||||||
event_decorators, service, config_per_platform, extract_domain_configs)
|
event_decorators, service, config_per_platform, extract_domain_configs)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
_SETUP_LOCK = RLock()
|
|
||||||
_CURRENT_SETUP = []
|
|
||||||
|
|
||||||
ATTR_COMPONENT = 'component'
|
ATTR_COMPONENT = 'component'
|
||||||
|
|
||||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||||
_PERSISTENT_PLATFORMS = set()
|
_PERSISTENT_ERRORS = {}
|
||||||
_PERSISTENT_VALIDATION = set()
|
HA_COMPONENT_URL = '[{}](https://home-assistant.io/components/{}/)'
|
||||||
|
|
||||||
|
|
||||||
def setup_component(hass: core.HomeAssistant, domain: str,
|
def setup_component(hass: core.HomeAssistant, domain: str,
|
||||||
config: Optional[Dict]=None) -> bool:
|
config: Optional[Dict]=None) -> bool:
|
||||||
"""Setup a component and all its dependencies."""
|
"""Setup a component and all its dependencies."""
|
||||||
|
return run_coroutine_threadsafe(
|
||||||
|
async_setup_component(hass, domain, config), loop=hass.loop).result()
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_component(hass: core.HomeAssistant, domain: str,
|
||||||
|
config: Optional[Dict]=None) -> bool:
|
||||||
|
"""Setup a component and all its dependencies.
|
||||||
|
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
if domain in hass.config.components:
|
if domain in hass.config.components:
|
||||||
_LOGGER.debug('Component %s already set up.', domain)
|
_LOGGER.debug('Component %s already set up.', domain)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
_ensure_loader_prepared(hass)
|
if not loader.PREPARED:
|
||||||
|
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||||
|
|
||||||
if config is None:
|
if config is None:
|
||||||
config = defaultdict(dict)
|
config = defaultdict(dict)
|
||||||
@ -52,11 +63,14 @@ def setup_component(hass: core.HomeAssistant, domain: str,
|
|||||||
|
|
||||||
# OrderedSet is empty if component or dependencies could not be resolved
|
# OrderedSet is empty if component or dependencies could not be resolved
|
||||||
if not components:
|
if not components:
|
||||||
|
_async_persistent_notification(hass, domain, True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for component in components:
|
for component in components:
|
||||||
if not _setup_component(hass, component, config):
|
res = yield from _async_setup_component(hass, component, config)
|
||||||
|
if not res:
|
||||||
_LOGGER.error('Component %s failed to setup', component)
|
_LOGGER.error('Component %s failed to setup', component)
|
||||||
|
_async_persistent_notification(hass, component, True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -64,7 +78,10 @@ def setup_component(hass: core.HomeAssistant, domain: str,
|
|||||||
|
|
||||||
def _handle_requirements(hass: core.HomeAssistant, component,
|
def _handle_requirements(hass: core.HomeAssistant, component,
|
||||||
name: str) -> bool:
|
name: str) -> bool:
|
||||||
"""Install the requirements for a component."""
|
"""Install the requirements for a component.
|
||||||
|
|
||||||
|
This method needs to run in an executor.
|
||||||
|
"""
|
||||||
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
|
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -72,70 +89,117 @@ def _handle_requirements(hass: core.HomeAssistant, component,
|
|||||||
if not pkg_util.install_package(req, target=hass.config.path('deps')):
|
if not pkg_util.install_package(req, target=hass.config.path('deps')):
|
||||||
_LOGGER.error('Not initializing %s because could not install '
|
_LOGGER.error('Not initializing %s because could not install '
|
||||||
'dependency %s', name, req)
|
'dependency %s', name, req)
|
||||||
|
_async_persistent_notification(hass, name)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
|
@asyncio.coroutine
|
||||||
"""Setup a component for Home Assistant."""
|
def _async_setup_component(hass: core.HomeAssistant,
|
||||||
# pylint: disable=too-many-return-statements,too-many-branches
|
domain: str, config) -> bool:
|
||||||
# pylint: disable=too-many-statements
|
"""Setup a component for Home Assistant.
|
||||||
|
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
|
# pylint: disable=too-many-return-statements
|
||||||
if domain in hass.config.components:
|
if domain in hass.config.components:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
with _SETUP_LOCK:
|
setup_lock = hass.data.get('setup_lock')
|
||||||
# It might have been loaded while waiting for lock
|
if setup_lock is None:
|
||||||
if domain in hass.config.components:
|
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
||||||
return True
|
|
||||||
|
|
||||||
if domain in _CURRENT_SETUP:
|
setup_progress = hass.data.get('setup_progress')
|
||||||
|
if setup_progress is None:
|
||||||
|
setup_progress = hass.data['setup_progress'] = []
|
||||||
|
|
||||||
|
if domain in setup_progress:
|
||||||
_LOGGER.error('Attempt made to setup %s during setup of %s',
|
_LOGGER.error('Attempt made to setup %s during setup of %s',
|
||||||
domain, domain)
|
domain, domain)
|
||||||
|
_async_persistent_notification(hass, domain, True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
config = prepare_setup_component(hass, config, domain)
|
try:
|
||||||
|
# Used to indicate to discovery that a setup is ongoing and allow it
|
||||||
|
# to wait till it is done.
|
||||||
|
did_lock = False
|
||||||
|
if not setup_lock.locked():
|
||||||
|
yield from setup_lock.acquire()
|
||||||
|
did_lock = True
|
||||||
|
|
||||||
|
setup_progress.append(domain)
|
||||||
|
config = yield from async_prepare_setup_component(hass, config, domain)
|
||||||
|
|
||||||
if config is None:
|
if config is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
component = loader.get_component(domain)
|
component = loader.get_component(domain)
|
||||||
_CURRENT_SETUP.append(domain)
|
if component is None:
|
||||||
|
_async_persistent_notification(hass, domain)
|
||||||
|
return False
|
||||||
|
|
||||||
|
async_comp = hasattr(component, 'async_setup')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = component.setup(hass, config)
|
if async_comp:
|
||||||
|
result = yield from component.async_setup(hass, config)
|
||||||
|
else:
|
||||||
|
result = yield from hass.loop.run_in_executor(
|
||||||
|
None, component.setup, hass, config)
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception('Error during setup of component %s', domain)
|
||||||
|
_async_persistent_notification(hass, domain, True)
|
||||||
|
return False
|
||||||
|
|
||||||
if result is False:
|
if result is False:
|
||||||
_LOGGER.error('component %s failed to initialize', domain)
|
_LOGGER.error('component %s failed to initialize', domain)
|
||||||
|
_async_persistent_notification(hass, domain, True)
|
||||||
return False
|
return False
|
||||||
elif result is not True:
|
elif result is not True:
|
||||||
_LOGGER.error('component %s did not return boolean if setup '
|
_LOGGER.error('component %s did not return boolean if setup '
|
||||||
'was successful. Disabling component.', domain)
|
'was successful. Disabling component.', domain)
|
||||||
|
_async_persistent_notification(hass, domain, True)
|
||||||
loader.set_component(domain, None)
|
loader.set_component(domain, None)
|
||||||
return False
|
return False
|
||||||
except Exception: # pylint: disable=broad-except
|
|
||||||
_LOGGER.exception('Error during setup of component %s', domain)
|
|
||||||
return False
|
|
||||||
finally:
|
|
||||||
_CURRENT_SETUP.remove(domain)
|
|
||||||
|
|
||||||
hass.config.components.append(component.DOMAIN)
|
hass.config.components.append(component.DOMAIN)
|
||||||
|
|
||||||
# Assumption: if a component does not depend on groups
|
# Assumption: if a component does not depend on groups
|
||||||
# it communicates with devices
|
# it communicates with devices
|
||||||
if 'group' not in getattr(component, 'DEPENDENCIES', []) and \
|
if (not async_comp and
|
||||||
hass.pool.worker_count <= 10:
|
'group' not in getattr(component, 'DEPENDENCIES', [])):
|
||||||
|
if hass.pool is None:
|
||||||
|
hass.async_init_pool()
|
||||||
|
if hass.pool.worker_count <= 10:
|
||||||
hass.pool.add_worker()
|
hass.pool.add_worker()
|
||||||
|
|
||||||
hass.bus.fire(
|
hass.bus.async_fire(
|
||||||
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
|
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
finally:
|
||||||
|
setup_progress.remove(domain)
|
||||||
|
if did_lock:
|
||||||
|
setup_lock.release()
|
||||||
|
|
||||||
|
|
||||||
def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||||
domain: str):
|
domain: str):
|
||||||
"""Prepare setup of a component and return processed config."""
|
"""Prepare setup of a component and return processed config."""
|
||||||
|
return run_coroutine_threadsafe(
|
||||||
|
async_prepare_setup_component(hass, config, domain), loop=hass.loop
|
||||||
|
).result()
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||||
|
domain: str):
|
||||||
|
"""Prepare setup of a component and return processed config.
|
||||||
|
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
# pylint: disable=too-many-return-statements
|
# pylint: disable=too-many-return-statements
|
||||||
component = loader.get_component(domain)
|
component = loader.get_component(domain)
|
||||||
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
|
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
|
||||||
@ -151,7 +215,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
|||||||
try:
|
try:
|
||||||
config = component.CONFIG_SCHEMA(config)
|
config = component.CONFIG_SCHEMA(config)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
log_exception(ex, domain, config, hass)
|
async_log_exception(ex, domain, config, hass)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
||||||
@ -161,7 +225,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
|||||||
try:
|
try:
|
||||||
p_validated = component.PLATFORM_SCHEMA(p_config)
|
p_validated = component.PLATFORM_SCHEMA(p_config)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
log_exception(ex, domain, config, hass)
|
async_log_exception(ex, domain, config, hass)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Not all platform components follow same pattern for platforms
|
# Not all platform components follow same pattern for platforms
|
||||||
@ -171,8 +235,8 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
|||||||
platforms.append(p_validated)
|
platforms.append(p_validated)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
platform = prepare_setup_platform(hass, config, domain,
|
platform = yield from async_prepare_setup_platform(
|
||||||
p_name)
|
hass, config, domain, p_name)
|
||||||
|
|
||||||
if platform is None:
|
if platform is None:
|
||||||
continue
|
continue
|
||||||
@ -180,9 +244,10 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
|||||||
# Validate platform specific schema
|
# Validate platform specific schema
|
||||||
if hasattr(platform, 'PLATFORM_SCHEMA'):
|
if hasattr(platform, 'PLATFORM_SCHEMA'):
|
||||||
try:
|
try:
|
||||||
|
# pylint: disable=no-member
|
||||||
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
log_exception(ex, '{}.{}'.format(domain, p_name),
|
async_log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||||
p_validated, hass)
|
p_validated, hass)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -195,7 +260,9 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
|||||||
if key not in filter_keys}
|
if key not in filter_keys}
|
||||||
config[domain] = platforms
|
config[domain] = platforms
|
||||||
|
|
||||||
if not _handle_requirements(hass, component, domain):
|
res = yield from hass.loop.run_in_executor(
|
||||||
|
None, _handle_requirements, hass, component, domain)
|
||||||
|
if not res:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return config
|
return config
|
||||||
@ -204,7 +271,22 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
|||||||
def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||||
platform_name: str) -> Optional[ModuleType]:
|
platform_name: str) -> Optional[ModuleType]:
|
||||||
"""Load a platform and makes sure dependencies are setup."""
|
"""Load a platform and makes sure dependencies are setup."""
|
||||||
_ensure_loader_prepared(hass)
|
return run_coroutine_threadsafe(
|
||||||
|
async_prepare_setup_platform(hass, config, domain, platform_name),
|
||||||
|
loop=hass.loop
|
||||||
|
).result()
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||||
|
platform_name: str) \
|
||||||
|
-> Optional[ModuleType]:
|
||||||
|
"""Load a platform and makes sure dependencies are setup.
|
||||||
|
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
|
if not loader.PREPARED:
|
||||||
|
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||||
|
|
||||||
platform_path = PLATFORM_FORMAT.format(domain, platform_name)
|
platform_path = PLATFORM_FORMAT.format(domain, platform_name)
|
||||||
|
|
||||||
@ -213,13 +295,7 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
|||||||
# Not found
|
# Not found
|
||||||
if platform is None:
|
if platform is None:
|
||||||
_LOGGER.error('Unable to find platform %s', platform_path)
|
_LOGGER.error('Unable to find platform %s', platform_path)
|
||||||
|
_async_persistent_notification(hass, platform_path)
|
||||||
_PERSISTENT_PLATFORMS.add(platform_path)
|
|
||||||
message = ('Unable to find the following platforms: ' +
|
|
||||||
', '.join(list(_PERSISTENT_PLATFORMS)) +
|
|
||||||
'(please check your configuration)')
|
|
||||||
persistent_notification.create(
|
|
||||||
hass, message, 'Invalid platforms', 'platform_errors')
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Already loaded
|
# Already loaded
|
||||||
@ -228,20 +304,23 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
|||||||
|
|
||||||
# Load dependencies
|
# Load dependencies
|
||||||
for component in getattr(platform, 'DEPENDENCIES', []):
|
for component in getattr(platform, 'DEPENDENCIES', []):
|
||||||
if not setup_component(hass, component, config):
|
res = yield from async_setup_component(hass, component, config)
|
||||||
|
if not res:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
'Unable to prepare setup for platform %s because '
|
'Unable to prepare setup for platform %s because '
|
||||||
'dependency %s could not be initialized', platform_path,
|
'dependency %s could not be initialized', platform_path,
|
||||||
component)
|
component)
|
||||||
|
_async_persistent_notification(hass, platform_path, True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not _handle_requirements(hass, platform, platform_path):
|
res = yield from hass.loop.run_in_executor(
|
||||||
|
None, _handle_requirements, hass, platform, platform_path)
|
||||||
|
if not res:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return platform
|
return platform
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
|
|
||||||
def from_config_dict(config: Dict[str, Any],
|
def from_config_dict(config: Dict[str, Any],
|
||||||
hass: Optional[core.HomeAssistant]=None,
|
hass: Optional[core.HomeAssistant]=None,
|
||||||
config_dir: Optional[str]=None,
|
config_dir: Optional[str]=None,
|
||||||
@ -261,15 +340,49 @@ def from_config_dict(config: Dict[str, Any],
|
|||||||
hass.config.config_dir = config_dir
|
hass.config.config_dir = config_dir
|
||||||
mount_local_lib_path(config_dir)
|
mount_local_lib_path(config_dir)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _async_init_from_config_dict(future):
|
||||||
|
try:
|
||||||
|
re_hass = yield from async_from_config_dict(
|
||||||
|
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||||
|
log_rotate_days)
|
||||||
|
future.set_result(re_hass)
|
||||||
|
# pylint: disable=broad-except
|
||||||
|
except Exception as exc:
|
||||||
|
future.set_exception(exc)
|
||||||
|
|
||||||
|
# run task
|
||||||
|
future = asyncio.Future(loop=hass.loop)
|
||||||
|
hass.loop.create_task(_async_init_from_config_dict(future))
|
||||||
|
hass.loop.run_until_complete(future)
|
||||||
|
|
||||||
|
return future.result()
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_from_config_dict(config: Dict[str, Any],
|
||||||
|
hass: core.HomeAssistant,
|
||||||
|
config_dir: Optional[str]=None,
|
||||||
|
enable_log: bool=True,
|
||||||
|
verbose: bool=False,
|
||||||
|
skip_pip: bool=False,
|
||||||
|
log_rotate_days: Any=None) \
|
||||||
|
-> Optional[core.HomeAssistant]:
|
||||||
|
"""Try to configure Home Assistant from a config dict.
|
||||||
|
|
||||||
|
Dynamically loads required components and its dependencies.
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
core_config = config.get(core.DOMAIN, {})
|
core_config = config.get(core.DOMAIN, {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conf_util.process_ha_core_config(hass, core_config)
|
yield from conf_util.async_process_ha_core_config(hass, core_config)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
log_exception(ex, 'homeassistant', core_config, hass)
|
async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
conf_util.process_ha_config_upgrade(hass)
|
yield from hass.loop.run_in_executor(
|
||||||
|
None, conf_util.process_ha_config_upgrade, hass)
|
||||||
|
|
||||||
if enable_log:
|
if enable_log:
|
||||||
enable_logging(hass, verbose, log_rotate_days)
|
enable_logging(hass, verbose, log_rotate_days)
|
||||||
@ -279,7 +392,8 @@ def from_config_dict(config: Dict[str, Any],
|
|||||||
_LOGGER.warning('Skipping pip installation of required modules. '
|
_LOGGER.warning('Skipping pip installation of required modules. '
|
||||||
'This may cause issues.')
|
'This may cause issues.')
|
||||||
|
|
||||||
_ensure_loader_prepared(hass)
|
if not loader.PREPARED:
|
||||||
|
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||||
|
|
||||||
# Make a copy because we are mutating it.
|
# Make a copy because we are mutating it.
|
||||||
# Convert it to defaultdict so components can always have config dict
|
# Convert it to defaultdict so components can always have config dict
|
||||||
@ -291,15 +405,15 @@ def from_config_dict(config: Dict[str, Any],
|
|||||||
components = set(key.split(' ')[0] for key in config.keys()
|
components = set(key.split(' ')[0] for key in config.keys()
|
||||||
if key != core.DOMAIN)
|
if key != core.DOMAIN)
|
||||||
|
|
||||||
# Setup in a thread to avoid blocking
|
# setup components
|
||||||
def component_setup():
|
# pylint: disable=not-an-iterable
|
||||||
"""Set up a component."""
|
res = yield from core_components.async_setup(hass, config)
|
||||||
if not core_components.setup(hass, config):
|
if not res:
|
||||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||||
'Further initialization aborted.')
|
'Further initialization aborted.')
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
persistent_notification.setup(hass, config)
|
yield from persistent_notification.async_setup(hass, config)
|
||||||
|
|
||||||
_LOGGER.info('Home Assistant core initialized')
|
_LOGGER.info('Home Assistant core initialized')
|
||||||
|
|
||||||
@ -309,11 +423,7 @@ def from_config_dict(config: Dict[str, Any],
|
|||||||
|
|
||||||
# Setup the components
|
# Setup the components
|
||||||
for domain in loader.load_order_components(components):
|
for domain in loader.load_order_components(components):
|
||||||
_setup_component(hass, domain, config)
|
yield from _async_setup_component(hass, domain, config)
|
||||||
|
|
||||||
hass.loop.run_until_complete(
|
|
||||||
hass.loop.run_in_executor(None, component_setup)
|
|
||||||
)
|
|
||||||
|
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
@ -331,27 +441,62 @@ def from_config_file(config_path: str,
|
|||||||
if hass is None:
|
if hass is None:
|
||||||
hass = core.HomeAssistant()
|
hass = core.HomeAssistant()
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _async_init_from_config_file(future):
|
||||||
|
try:
|
||||||
|
re_hass = yield from async_from_config_file(
|
||||||
|
config_path, hass, verbose, skip_pip, log_rotate_days)
|
||||||
|
future.set_result(re_hass)
|
||||||
|
# pylint: disable=broad-except
|
||||||
|
except Exception as exc:
|
||||||
|
future.set_exception(exc)
|
||||||
|
|
||||||
|
# run task
|
||||||
|
future = asyncio.Future(loop=hass.loop)
|
||||||
|
hass.loop.create_task(_async_init_from_config_file(future))
|
||||||
|
hass.loop.run_until_complete(future)
|
||||||
|
|
||||||
|
return future.result()
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_from_config_file(config_path: str,
|
||||||
|
hass: core.HomeAssistant,
|
||||||
|
verbose: bool=False,
|
||||||
|
skip_pip: bool=True,
|
||||||
|
log_rotate_days: Any=None):
|
||||||
|
"""Read the configuration file and try to start all the functionality.
|
||||||
|
|
||||||
|
Will add functionality to 'hass' parameter.
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
# Set config dir to directory holding config file
|
# Set config dir to directory holding config file
|
||||||
config_dir = os.path.abspath(os.path.dirname(config_path))
|
config_dir = os.path.abspath(os.path.dirname(config_path))
|
||||||
hass.config.config_dir = config_dir
|
hass.config.config_dir = config_dir
|
||||||
mount_local_lib_path(config_dir)
|
yield from hass.loop.run_in_executor(
|
||||||
|
None, mount_local_lib_path, config_dir)
|
||||||
|
|
||||||
enable_logging(hass, verbose, log_rotate_days)
|
enable_logging(hass, verbose, log_rotate_days)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config_dict = conf_util.load_yaml_config_file(config_path)
|
config_dict = yield from hass.loop.run_in_executor(
|
||||||
|
None, conf_util.load_yaml_config_file, config_path)
|
||||||
except HomeAssistantError:
|
except HomeAssistantError:
|
||||||
return None
|
return None
|
||||||
finally:
|
finally:
|
||||||
clear_secret_cache()
|
clear_secret_cache()
|
||||||
|
|
||||||
return from_config_dict(config_dict, hass, enable_log=False,
|
hass = yield from async_from_config_dict(
|
||||||
skip_pip=skip_pip)
|
config_dict, hass, enable_log=False, skip_pip=skip_pip)
|
||||||
|
return hass
|
||||||
|
|
||||||
|
|
||||||
def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||||
log_rotate_days=None) -> None:
|
log_rotate_days=None) -> None:
|
||||||
"""Setup the logging."""
|
"""Setup the logging.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
|
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
|
||||||
"[%(name)s] %(message)s%(reset)s")
|
"[%(name)s] %(message)s%(reset)s")
|
||||||
@ -359,6 +504,7 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
|||||||
# suppress overly verbose logs from libraries that aren't helpful
|
# suppress overly verbose logs from libraries that aren't helpful
|
||||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||||
|
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from colorlog import ColoredFormatter
|
from colorlog import ColoredFormatter
|
||||||
@ -406,33 +552,49 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
|||||||
'Unable to setup error log %s (access denied)', err_log_path)
|
'Unable to setup error log %s (access denied)', err_log_path)
|
||||||
|
|
||||||
|
|
||||||
def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
|
def log_exception(ex, domain, config, hass):
|
||||||
"""Ensure Home Assistant loader is prepared."""
|
|
||||||
if not loader.PREPARED:
|
|
||||||
loader.prepare(hass)
|
|
||||||
|
|
||||||
|
|
||||||
def log_exception(ex, domain, config, hass=None):
|
|
||||||
"""Generate log exception for config validation."""
|
"""Generate log exception for config validation."""
|
||||||
|
run_callback_threadsafe(
|
||||||
|
hass.loop, async_log_exception, ex, domain, config, hass).result()
|
||||||
|
|
||||||
|
|
||||||
|
@core.callback
|
||||||
|
def _async_persistent_notification(hass: core.HomeAssistant, component: str,
|
||||||
|
link: Optional[bool]=False):
|
||||||
|
"""Print a persistent notification.
|
||||||
|
|
||||||
|
This method must be run in the event loop.
|
||||||
|
"""
|
||||||
|
_PERSISTENT_ERRORS[component] = _PERSISTENT_ERRORS.get(component) or link
|
||||||
|
_lst = [HA_COMPONENT_URL.format(name.replace('_', '-'), name)
|
||||||
|
if link else name for name, link in _PERSISTENT_ERRORS.items()]
|
||||||
|
message = ('The following components and platforms could not be set up:\n'
|
||||||
|
'* ' + '\n* '.join(list(_lst)) + '\nPlease check your config')
|
||||||
|
persistent_notification.async_create(
|
||||||
|
hass, message, 'Invalid config', 'invalid_config')
|
||||||
|
|
||||||
|
|
||||||
|
@core.callback
|
||||||
|
def async_log_exception(ex, domain, config, hass):
|
||||||
|
"""Generate log exception for config validation.
|
||||||
|
|
||||||
|
This method must be run in the event loop.
|
||||||
|
"""
|
||||||
message = 'Invalid config for [{}]: '.format(domain)
|
message = 'Invalid config for [{}]: '.format(domain)
|
||||||
if hass is not None:
|
if hass is not None:
|
||||||
_PERSISTENT_VALIDATION.add(domain)
|
_async_persistent_notification(hass, domain, True)
|
||||||
message = ('The following platforms contain invalid configuration: ' +
|
|
||||||
', '.join(list(_PERSISTENT_VALIDATION)) +
|
|
||||||
' (please check your configuration)')
|
|
||||||
persistent_notification.create(
|
|
||||||
hass, message, 'Invalid config', 'invalid_config')
|
|
||||||
|
|
||||||
if 'extra keys not allowed' in ex.error_message:
|
if 'extra keys not allowed' in ex.error_message:
|
||||||
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
|
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
|
||||||
.format(ex.path[-1], domain, domain,
|
.format(ex.path[-1], domain, domain,
|
||||||
'->'.join('%s' % m for m in ex.path))
|
'->'.join(str(m) for m in ex.path))
|
||||||
else:
|
else:
|
||||||
message += '{}.'.format(humanize_error(config, ex))
|
message += '{}.'.format(humanize_error(config, ex))
|
||||||
|
|
||||||
if hasattr(config, '__line__'):
|
domain_config = config.get(domain, config)
|
||||||
message += " (See {}:{})".format(
|
message += " (See {}:{}). ".format(
|
||||||
config.__config_file__, config.__line__ or '?')
|
getattr(domain_config, '__config_file__', '?'),
|
||||||
|
getattr(domain_config, '__line__', '?'))
|
||||||
|
|
||||||
if domain != 'homeassistant':
|
if domain != 'homeassistant':
|
||||||
message += ('Please check the docs at '
|
message += ('Please check the docs at '
|
||||||
@ -442,7 +604,10 @@ def log_exception(ex, domain, config, hass=None):
|
|||||||
|
|
||||||
|
|
||||||
def mount_local_lib_path(config_dir: str) -> str:
|
def mount_local_lib_path(config_dir: str) -> str:
|
||||||
"""Add local library to Python Path."""
|
"""Add local library to Python Path.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
deps_dir = os.path.join(config_dir, 'deps')
|
deps_dir = os.path.join(config_dir, 'deps')
|
||||||
if deps_dir not in sys.path:
|
if deps_dir not in sys.path:
|
||||||
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
||||||
|
@ -7,6 +7,7 @@ Component design guidelines:
|
|||||||
format "<DOMAIN>.<OBJECT_ID>".
|
format "<DOMAIN>.<OBJECT_ID>".
|
||||||
- Each component should publish services only under its own domain.
|
- Each component should publish services only under its own domain.
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import itertools as it
|
import itertools as it
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -79,8 +80,10 @@ def reload_core_config(hass):
|
|||||||
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
@asyncio.coroutine
|
||||||
|
def async_setup(hass, config):
|
||||||
"""Setup general services related to Home Assistant."""
|
"""Setup general services related to Home Assistant."""
|
||||||
|
@asyncio.coroutine
|
||||||
def handle_turn_service(service):
|
def handle_turn_service(service):
|
||||||
"""Method to handle calls to homeassistant.turn_on/off."""
|
"""Method to handle calls to homeassistant.turn_on/off."""
|
||||||
entity_ids = extract_entity_ids(hass, service)
|
entity_ids = extract_entity_ids(hass, service)
|
||||||
@ -96,6 +99,8 @@ def setup(hass, config):
|
|||||||
by_domain = it.groupby(sorted(entity_ids),
|
by_domain = it.groupby(sorted(entity_ids),
|
||||||
lambda item: ha.split_entity_id(item)[0])
|
lambda item: ha.split_entity_id(item)[0])
|
||||||
|
|
||||||
|
tasks = []
|
||||||
|
|
||||||
for domain, ent_ids in by_domain:
|
for domain, ent_ids in by_domain:
|
||||||
# We want to block for all calls and only return when all calls
|
# We want to block for all calls and only return when all calls
|
||||||
# have been processed. If a service does not exist it causes a 10
|
# have been processed. If a service does not exist it causes a 10
|
||||||
@ -111,27 +116,34 @@ def setup(hass, config):
|
|||||||
# ent_ids is a generator, convert it to a list.
|
# ent_ids is a generator, convert it to a list.
|
||||||
data[ATTR_ENTITY_ID] = list(ent_ids)
|
data[ATTR_ENTITY_ID] = list(ent_ids)
|
||||||
|
|
||||||
hass.services.call(domain, service.service, data, blocking)
|
tasks.append(hass.services.async_call(
|
||||||
|
domain, service.service, data, blocking))
|
||||||
|
|
||||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
|
||||||
hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
|
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
||||||
|
hass.services.async_register(
|
||||||
|
ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
||||||
|
hass.services.async_register(
|
||||||
|
ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def handle_reload_config(call):
|
def handle_reload_config(call):
|
||||||
"""Service handler for reloading core config."""
|
"""Service handler for reloading core config."""
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant import config as conf_util
|
from homeassistant import config as conf_util
|
||||||
|
|
||||||
try:
|
try:
|
||||||
path = conf_util.find_config_file(hass.config.config_dir)
|
conf = yield from conf_util.async_hass_config_yaml(hass)
|
||||||
conf = conf_util.load_yaml_config_file(path)
|
|
||||||
except HomeAssistantError as err:
|
except HomeAssistantError as err:
|
||||||
_LOGGER.error(err)
|
_LOGGER.error(err)
|
||||||
return
|
return
|
||||||
|
|
||||||
conf_util.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {})
|
yield from conf_util.async_process_ha_core_config(
|
||||||
|
hass, conf.get(ha.DOMAIN) or {})
|
||||||
|
|
||||||
hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG,
|
hass.services.async_register(
|
||||||
handle_reload_config)
|
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, handle_reload_config)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -42,8 +42,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
add_devices([AlarmDotCom(hass, name, code, username, password)])
|
add_devices([AlarmDotCom(hass, name, code, username, password)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class AlarmDotCom(alarm.AlarmControlPanel):
|
class AlarmDotCom(alarm.AlarmControlPanel):
|
||||||
"""Represent an Alarm.com status."""
|
"""Represent an Alarm.com status."""
|
||||||
|
|
||||||
|
@ -4,23 +4,19 @@ Support for Concord232 alarm control panels.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alarm_control_panel.concord232/
|
https://home-assistant.io/components/alarm_control_panel.concord232/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_NAME, CONF_PORT,
|
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||||
STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
REQUIREMENTS = ['concord232==0.14']
|
REQUIREMENTS = ['concord232==0.14']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -29,17 +25,17 @@ DEFAULT_HOST = 'localhost'
|
|||||||
DEFAULT_NAME = 'CONCORD232'
|
DEFAULT_NAME = 'CONCORD232'
|
||||||
DEFAULT_PORT = 5007
|
DEFAULT_PORT = 5007
|
||||||
|
|
||||||
|
SCAN_INTERVAL = 1
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
SCAN_INTERVAL = 1
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup concord232 platform."""
|
"""Set up the Concord232 alarm control panel platform."""
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
host = config.get(CONF_HOST)
|
host = config.get(CONF_HOST)
|
||||||
port = config.get(CONF_PORT)
|
port = config.get(CONF_PORT)
|
||||||
@ -49,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
try:
|
try:
|
||||||
add_devices([Concord232Alarm(hass, url, name)])
|
add_devices([Concord232Alarm(hass, url, name)])
|
||||||
except requests.exceptions.ConnectionError as ex:
|
except requests.exceptions.ConnectionError as ex:
|
||||||
_LOGGER.error('Unable to connect to Concord232: %s', str(ex))
|
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@ -57,7 +53,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
|||||||
"""Represents the Concord232-based alarm panel."""
|
"""Represents the Concord232-based alarm panel."""
|
||||||
|
|
||||||
def __init__(self, hass, url, name):
|
def __init__(self, hass, url, name):
|
||||||
"""Initalize the concord232 alarm panel."""
|
"""Initialize the Concord232 alarm panel."""
|
||||||
from concord232 import client as concord232_client
|
from concord232 import client as concord232_client
|
||||||
|
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
@ -68,7 +64,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
|||||||
try:
|
try:
|
||||||
client = concord232_client.Client(self._url)
|
client = concord232_client.Client(self._url)
|
||||||
except requests.exceptions.ConnectionError as ex:
|
except requests.exceptions.ConnectionError as ex:
|
||||||
_LOGGER.error('Unable to connect to Concord232: %s', str(ex))
|
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||||
|
|
||||||
self._alarm = client
|
self._alarm = client
|
||||||
self._alarm.partitions = self._alarm.list_partitions()
|
self._alarm.partitions = self._alarm.list_partitions()
|
||||||
@ -100,16 +96,16 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
|||||||
try:
|
try:
|
||||||
part = self._alarm.list_partitions()[0]
|
part = self._alarm.list_partitions()[0]
|
||||||
except requests.exceptions.ConnectionError as ex:
|
except requests.exceptions.ConnectionError as ex:
|
||||||
_LOGGER.error('Unable to connect to %(host)s: %(reason)s',
|
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
|
||||||
dict(host=self._url, reason=ex))
|
dict(host=self._url, reason=ex))
|
||||||
newstate = STATE_UNKNOWN
|
newstate = STATE_UNKNOWN
|
||||||
except IndexError:
|
except IndexError:
|
||||||
_LOGGER.error('concord232 reports no partitions')
|
_LOGGER.error("Concord232 reports no partitions")
|
||||||
newstate = STATE_UNKNOWN
|
newstate = STATE_UNKNOWN
|
||||||
|
|
||||||
if part['arming_level'] == "Off":
|
if part['arming_level'] == 'Off':
|
||||||
newstate = STATE_ALARM_DISARMED
|
newstate = STATE_ALARM_DISARMED
|
||||||
elif "Home" in part['arming_level']:
|
elif 'Home' in part['arming_level']:
|
||||||
newstate = STATE_ALARM_ARMED_HOME
|
newstate = STATE_ALARM_ARMED_HOME
|
||||||
else:
|
else:
|
||||||
newstate = STATE_ALARM_ARMED_AWAY
|
newstate = STATE_ALARM_ARMED_AWAY
|
||||||
|
@ -5,25 +5,22 @@ For more details about this platform, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.envisalink import (EVL_CONTROLLER,
|
from homeassistant.components.envisalink import (
|
||||||
EnvisalinkDevice,
|
EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
||||||
PARTITION_SCHEMA,
|
CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE)
|
||||||
CONF_CODE,
|
|
||||||
CONF_PANIC,
|
|
||||||
CONF_PARTITIONNAME,
|
|
||||||
SIGNAL_PARTITION_UPDATE,
|
|
||||||
SIGNAL_KEYPAD_UPDATE)
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
||||||
|
|
||||||
DEPENDENCIES = ['envisalink']
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['envisalink']
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Perform the setup for Envisalink alarm panels."""
|
"""Perform the setup for Envisalink alarm panels."""
|
||||||
_configured_partitions = discovery_info['partitions']
|
_configured_partitions = discovery_info['partitions']
|
||||||
_code = discovery_info[CONF_CODE]
|
_code = discovery_info[CONF_CODE]
|
||||||
@ -38,29 +35,28 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
_panic_type,
|
_panic_type,
|
||||||
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
||||||
EVL_CONTROLLER)
|
EVL_CONTROLLER)
|
||||||
add_devices_callback([_device])
|
add_devices([_device])
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||||
"""Represents the Envisalink-based alarm panel."""
|
"""Representation of an Envisalink-based alarm panel."""
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
def __init__(self, partition_number, alarm_name, code, panic_type, info,
|
||||||
def __init__(self, partition_number, alarm_name,
|
controller):
|
||||||
code, panic_type, info, controller):
|
|
||||||
"""Initialize the alarm panel."""
|
"""Initialize the alarm panel."""
|
||||||
from pydispatch import dispatcher
|
from pydispatch import dispatcher
|
||||||
self._partition_number = partition_number
|
self._partition_number = partition_number
|
||||||
self._code = code
|
self._code = code
|
||||||
self._panic_type = panic_type
|
self._panic_type = panic_type
|
||||||
_LOGGER.debug('Setting up alarm: ' + alarm_name)
|
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||||
EnvisalinkDevice.__init__(self, alarm_name, info, controller)
|
EnvisalinkDevice.__init__(self, alarm_name, info, controller)
|
||||||
dispatcher.connect(self._update_callback,
|
dispatcher.connect(
|
||||||
signal=SIGNAL_PARTITION_UPDATE,
|
self._update_callback, signal=SIGNAL_PARTITION_UPDATE,
|
||||||
sender=dispatcher.Any)
|
sender=dispatcher.Any)
|
||||||
dispatcher.connect(self._update_callback,
|
dispatcher.connect(
|
||||||
signal=SIGNAL_KEYPAD_UPDATE,
|
self._update_callback, signal=SIGNAL_KEYPAD_UPDATE,
|
||||||
sender=dispatcher.Any)
|
sender=dispatcher.Any)
|
||||||
|
|
||||||
def _update_callback(self, partition):
|
def _update_callback(self, partition):
|
||||||
@ -90,20 +86,20 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
|||||||
def alarm_disarm(self, code=None):
|
def alarm_disarm(self, code=None):
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
if self._code:
|
if self._code:
|
||||||
EVL_CONTROLLER.disarm_partition(str(code),
|
EVL_CONTROLLER.disarm_partition(
|
||||||
self._partition_number)
|
str(code), self._partition_number)
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
if self._code:
|
if self._code:
|
||||||
EVL_CONTROLLER.arm_stay_partition(str(code),
|
EVL_CONTROLLER.arm_stay_partition(
|
||||||
self._partition_number)
|
str(code), self._partition_number)
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
if self._code:
|
if self._code:
|
||||||
EVL_CONTROLLER.arm_away_partition(str(code),
|
EVL_CONTROLLER.arm_away_partition(
|
||||||
self._partition_number)
|
str(code), self._partition_number)
|
||||||
|
|
||||||
def alarm_trigger(self, code=None):
|
def alarm_trigger(self, code=None):
|
||||||
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
||||||
|
@ -50,8 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
)])
|
)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class ManualAlarm(alarm.AlarmControlPanel):
|
class ManualAlarm(alarm.AlarmControlPanel):
|
||||||
"""
|
"""
|
||||||
Represents an alarm status.
|
Represents an alarm status.
|
||||||
|
@ -55,8 +55,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
config.get(CONF_CODE))])
|
config.get(CONF_CODE))])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class MqttAlarm(alarm.AlarmControlPanel):
|
class MqttAlarm(alarm.AlarmControlPanel):
|
||||||
"""Representation of a MQTT alarm status."""
|
"""Representation of a MQTT alarm status."""
|
||||||
|
|
||||||
|
@ -41,7 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
add_devices([SimpliSafeAlarm(name, username, password, code)])
|
add_devices([SimpliSafeAlarm(name, username, password, code)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||||
"""Representation a SimpliSafe alarm."""
|
"""Representation a SimpliSafe alarm."""
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
add_devices(alarms)
|
add_devices(alarms)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class VerisureAlarm(alarm.AlarmControlPanel):
|
class VerisureAlarm(alarm.AlarmControlPanel):
|
||||||
"""Represent a Verisure alarm status."""
|
"""Represent a Verisure alarm status."""
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ Support for Alexa skill service end point.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alexa/
|
https://home-assistant.io/components/alexa/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import copy
|
import copy
|
||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
@ -12,6 +13,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import HTTP_BAD_REQUEST
|
from homeassistant.const import HTTP_BAD_REQUEST
|
||||||
from homeassistant.helpers import template, script, config_validation as cv
|
from homeassistant.helpers import template, script, config_validation as cv
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
@ -20,7 +22,7 @@ import homeassistant.util.dt as dt_util
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
INTENTS_API_ENDPOINT = '/api/alexa'
|
INTENTS_API_ENDPOINT = '/api/alexa'
|
||||||
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/<briefing_id>'
|
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}'
|
||||||
|
|
||||||
CONF_ACTION = 'action'
|
CONF_ACTION = 'action'
|
||||||
CONF_CARD = 'card'
|
CONF_CARD = 'card'
|
||||||
@ -102,8 +104,8 @@ def setup(hass, config):
|
|||||||
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
||||||
flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {})
|
flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {})
|
||||||
|
|
||||||
hass.wsgi.register_view(AlexaIntentsView(hass, intents))
|
hass.http.register_view(AlexaIntentsView(hass, intents))
|
||||||
hass.wsgi.register_view(AlexaFlashBriefingView(hass, flash_briefings))
|
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefings))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -128,9 +130,10 @@ class AlexaIntentsView(HomeAssistantView):
|
|||||||
|
|
||||||
self.intents = intents
|
self.intents = intents
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Handle Alexa."""
|
"""Handle Alexa."""
|
||||||
data = request.json
|
data = yield from request.json()
|
||||||
|
|
||||||
_LOGGER.debug('Received Alexa request: %s', data)
|
_LOGGER.debug('Received Alexa request: %s', data)
|
||||||
|
|
||||||
@ -176,7 +179,7 @@ class AlexaIntentsView(HomeAssistantView):
|
|||||||
action = config.get(CONF_ACTION)
|
action = config.get(CONF_ACTION)
|
||||||
|
|
||||||
if action is not None:
|
if action is not None:
|
||||||
action.run(response.variables)
|
yield from action.async_run(response.variables)
|
||||||
|
|
||||||
# pylint: disable=unsubscriptable-object
|
# pylint: disable=unsubscriptable-object
|
||||||
if speech is not None:
|
if speech is not None:
|
||||||
@ -218,8 +221,8 @@ class AlexaResponse(object):
|
|||||||
self.card = card
|
self.card = card
|
||||||
return
|
return
|
||||||
|
|
||||||
card["title"] = title.render(self.variables)
|
card["title"] = title.async_render(self.variables)
|
||||||
card["content"] = content.render(self.variables)
|
card["content"] = content.async_render(self.variables)
|
||||||
self.card = card
|
self.card = card
|
||||||
|
|
||||||
def add_speech(self, speech_type, text):
|
def add_speech(self, speech_type, text):
|
||||||
@ -229,7 +232,7 @@ class AlexaResponse(object):
|
|||||||
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
||||||
|
|
||||||
if isinstance(text, template.Template):
|
if isinstance(text, template.Template):
|
||||||
text = text.render(self.variables)
|
text = text.async_render(self.variables)
|
||||||
|
|
||||||
self.speech = {
|
self.speech = {
|
||||||
'type': speech_type.value,
|
'type': speech_type.value,
|
||||||
@ -244,7 +247,7 @@ class AlexaResponse(object):
|
|||||||
|
|
||||||
self.reprompt = {
|
self.reprompt = {
|
||||||
'type': speech_type.value,
|
'type': speech_type.value,
|
||||||
key: text.render(self.variables)
|
key: text.async_render(self.variables)
|
||||||
}
|
}
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
@ -283,7 +286,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
|
|||||||
self.flash_briefings = copy.deepcopy(flash_briefings)
|
self.flash_briefings = copy.deepcopy(flash_briefings)
|
||||||
template.attach(hass, self.flash_briefings)
|
template.attach(hass, self.flash_briefings)
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
@callback
|
||||||
def get(self, request, briefing_id):
|
def get(self, request, briefing_id):
|
||||||
"""Handle Alexa Flash Briefing request."""
|
"""Handle Alexa Flash Briefing request."""
|
||||||
_LOGGER.debug('Received Alexa flash briefing request for: %s',
|
_LOGGER.debug('Received Alexa flash briefing request for: %s',
|
||||||
@ -292,7 +295,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
|
|||||||
if self.flash_briefings.get(briefing_id) is None:
|
if self.flash_briefings.get(briefing_id) is None:
|
||||||
err = 'No configured Alexa flash briefing was found for: %s'
|
err = 'No configured Alexa flash briefing was found for: %s'
|
||||||
_LOGGER.error(err, briefing_id)
|
_LOGGER.error(err, briefing_id)
|
||||||
return self.Response(status=404)
|
return b'', 404
|
||||||
|
|
||||||
briefing = []
|
briefing = []
|
||||||
|
|
||||||
@ -300,13 +303,13 @@ class AlexaFlashBriefingView(HomeAssistantView):
|
|||||||
output = {}
|
output = {}
|
||||||
if item.get(CONF_TITLE) is not None:
|
if item.get(CONF_TITLE) is not None:
|
||||||
if isinstance(item.get(CONF_TITLE), template.Template):
|
if isinstance(item.get(CONF_TITLE), template.Template):
|
||||||
output[ATTR_TITLE_TEXT] = item[CONF_TITLE].render()
|
output[ATTR_TITLE_TEXT] = item[CONF_TITLE].async_render()
|
||||||
else:
|
else:
|
||||||
output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE)
|
output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE)
|
||||||
|
|
||||||
if item.get(CONF_TEXT) is not None:
|
if item.get(CONF_TEXT) is not None:
|
||||||
if isinstance(item.get(CONF_TEXT), template.Template):
|
if isinstance(item.get(CONF_TEXT), template.Template):
|
||||||
output[ATTR_MAIN_TEXT] = item[CONF_TEXT].render()
|
output[ATTR_MAIN_TEXT] = item[CONF_TEXT].async_render()
|
||||||
else:
|
else:
|
||||||
output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT)
|
output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT)
|
||||||
|
|
||||||
@ -315,7 +318,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
|
|||||||
|
|
||||||
if item.get(CONF_AUDIO) is not None:
|
if item.get(CONF_AUDIO) is not None:
|
||||||
if isinstance(item.get(CONF_AUDIO), template.Template):
|
if isinstance(item.get(CONF_AUDIO), template.Template):
|
||||||
output[ATTR_STREAM_URL] = item[CONF_AUDIO].render()
|
output[ATTR_STREAM_URL] = item[CONF_AUDIO].async_render()
|
||||||
else:
|
else:
|
||||||
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
|
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
|
||||||
|
|
||||||
@ -323,7 +326,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
|
|||||||
if isinstance(item.get(CONF_DISPLAY_URL),
|
if isinstance(item.get(CONF_DISPLAY_URL),
|
||||||
template.Template):
|
template.Template):
|
||||||
output[ATTR_REDIRECTION_URL] = \
|
output[ATTR_REDIRECTION_URL] = \
|
||||||
item[CONF_DISPLAY_URL].render()
|
item[CONF_DISPLAY_URL].async_render()
|
||||||
else:
|
else:
|
||||||
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
|
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
|
||||||
|
|
||||||
|
@ -7,7 +7,9 @@ https://home-assistant.io/developers/api/
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import queue
|
|
||||||
|
from aiohttp import web
|
||||||
|
import async_timeout
|
||||||
|
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
import homeassistant.remote as rem
|
import homeassistant.remote as rem
|
||||||
@ -21,7 +23,7 @@ from homeassistant.const import (
|
|||||||
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
|
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
|
||||||
__version__)
|
__version__)
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers.state import TrackStates
|
from homeassistant.helpers.state import AsyncTrackStates
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import template
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
@ -36,20 +38,20 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Register the API with the HTTP interface."""
|
"""Register the API with the HTTP interface."""
|
||||||
hass.wsgi.register_view(APIStatusView)
|
hass.http.register_view(APIStatusView)
|
||||||
hass.wsgi.register_view(APIEventStream)
|
hass.http.register_view(APIEventStream)
|
||||||
hass.wsgi.register_view(APIConfigView)
|
hass.http.register_view(APIConfigView)
|
||||||
hass.wsgi.register_view(APIDiscoveryView)
|
hass.http.register_view(APIDiscoveryView)
|
||||||
hass.wsgi.register_view(APIStatesView)
|
hass.http.register_view(APIStatesView)
|
||||||
hass.wsgi.register_view(APIEntityStateView)
|
hass.http.register_view(APIEntityStateView)
|
||||||
hass.wsgi.register_view(APIEventListenersView)
|
hass.http.register_view(APIEventListenersView)
|
||||||
hass.wsgi.register_view(APIEventView)
|
hass.http.register_view(APIEventView)
|
||||||
hass.wsgi.register_view(APIServicesView)
|
hass.http.register_view(APIServicesView)
|
||||||
hass.wsgi.register_view(APIDomainServicesView)
|
hass.http.register_view(APIDomainServicesView)
|
||||||
hass.wsgi.register_view(APIEventForwardingView)
|
hass.http.register_view(APIEventForwardingView)
|
||||||
hass.wsgi.register_view(APIComponentsView)
|
hass.http.register_view(APIComponentsView)
|
||||||
hass.wsgi.register_view(APIErrorLogView)
|
hass.http.register_view(APIErrorLogView)
|
||||||
hass.wsgi.register_view(APITemplateView)
|
hass.http.register_view(APITemplateView)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -60,6 +62,7 @@ class APIStatusView(HomeAssistantView):
|
|||||||
url = URL_API
|
url = URL_API
|
||||||
name = "api:status"
|
name = "api:status"
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Retrieve if API is running."""
|
"""Retrieve if API is running."""
|
||||||
return self.json_message('API running.')
|
return self.json_message('API running.')
|
||||||
@ -71,12 +74,13 @@ class APIEventStream(HomeAssistantView):
|
|||||||
url = URL_API_STREAM
|
url = URL_API_STREAM
|
||||||
name = "api:stream"
|
name = "api:stream"
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Provide a streaming interface for the event bus."""
|
"""Provide a streaming interface for the event bus."""
|
||||||
stop_obj = object()
|
stop_obj = object()
|
||||||
to_write = queue.Queue()
|
to_write = asyncio.Queue(loop=self.hass.loop)
|
||||||
|
|
||||||
restrict = request.args.get('restrict')
|
restrict = request.GET.get('restrict')
|
||||||
if restrict:
|
if restrict:
|
||||||
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
||||||
|
|
||||||
@ -96,21 +100,25 @@ class APIEventStream(HomeAssistantView):
|
|||||||
else:
|
else:
|
||||||
data = json.dumps(event, cls=rem.JSONEncoder)
|
data = json.dumps(event, cls=rem.JSONEncoder)
|
||||||
|
|
||||||
to_write.put(data)
|
yield from to_write.put(data)
|
||||||
|
|
||||||
def stream():
|
response = web.StreamResponse()
|
||||||
"""Stream events to response."""
|
response.content_type = 'text/event-stream'
|
||||||
unsub_stream = self.hass.bus.listen(MATCH_ALL, forward_events)
|
yield from response.prepare(request)
|
||||||
|
|
||||||
|
unsub_stream = self.hass.bus.async_listen(MATCH_ALL, forward_events)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||||
|
|
||||||
# Fire off one message so browsers fire open event right away
|
# Fire off one message so browsers fire open event right away
|
||||||
to_write.put(STREAM_PING_PAYLOAD)
|
yield from to_write.put(STREAM_PING_PAYLOAD)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
payload = to_write.get(timeout=STREAM_PING_INTERVAL)
|
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
||||||
|
loop=self.hass.loop):
|
||||||
|
payload = yield from to_write.get()
|
||||||
|
|
||||||
if payload is stop_obj:
|
if payload is stop_obj:
|
||||||
break
|
break
|
||||||
@ -118,17 +126,15 @@ class APIEventStream(HomeAssistantView):
|
|||||||
msg = "data: {}\n\n".format(payload)
|
msg = "data: {}\n\n".format(payload)
|
||||||
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||||
msg.strip())
|
msg.strip())
|
||||||
yield msg.encode("UTF-8")
|
response.write(msg.encode("UTF-8"))
|
||||||
except queue.Empty:
|
yield from response.drain()
|
||||||
to_write.put(STREAM_PING_PAYLOAD)
|
except asyncio.TimeoutError:
|
||||||
except GeneratorExit:
|
yield from to_write.put(STREAM_PING_PAYLOAD)
|
||||||
break
|
|
||||||
finally:
|
finally:
|
||||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||||
unsub_stream()
|
unsub_stream()
|
||||||
|
|
||||||
return self.Response(stream(), mimetype='text/event-stream')
|
|
||||||
|
|
||||||
|
|
||||||
class APIConfigView(HomeAssistantView):
|
class APIConfigView(HomeAssistantView):
|
||||||
"""View to handle Config requests."""
|
"""View to handle Config requests."""
|
||||||
@ -136,6 +142,7 @@ class APIConfigView(HomeAssistantView):
|
|||||||
url = URL_API_CONFIG
|
url = URL_API_CONFIG
|
||||||
name = "api:config"
|
name = "api:config"
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get current configuration."""
|
"""Get current configuration."""
|
||||||
return self.json(self.hass.config.as_dict())
|
return self.json(self.hass.config.as_dict())
|
||||||
@ -148,6 +155,7 @@ class APIDiscoveryView(HomeAssistantView):
|
|||||||
url = URL_API_DISCOVERY_INFO
|
url = URL_API_DISCOVERY_INFO
|
||||||
name = "api:discovery"
|
name = "api:discovery"
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get discovery info."""
|
"""Get discovery info."""
|
||||||
needs_auth = self.hass.config.api.api_password is not None
|
needs_auth = self.hass.config.api.api_password is not None
|
||||||
@ -165,17 +173,19 @@ class APIStatesView(HomeAssistantView):
|
|||||||
url = URL_API_STATES
|
url = URL_API_STATES
|
||||||
name = "api:states"
|
name = "api:states"
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get current states."""
|
"""Get current states."""
|
||||||
return self.json(self.hass.states.all())
|
return self.json(self.hass.states.async_all())
|
||||||
|
|
||||||
|
|
||||||
class APIEntityStateView(HomeAssistantView):
|
class APIEntityStateView(HomeAssistantView):
|
||||||
"""View to handle EntityState requests."""
|
"""View to handle EntityState requests."""
|
||||||
|
|
||||||
url = "/api/states/<entity(exist=False):entity_id>"
|
url = "/api/states/{entity_id}"
|
||||||
name = "api:entity-state"
|
name = "api:entity-state"
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
def get(self, request, entity_id):
|
def get(self, request, entity_id):
|
||||||
"""Retrieve state of entity."""
|
"""Retrieve state of entity."""
|
||||||
state = self.hass.states.get(entity_id)
|
state = self.hass.states.get(entity_id)
|
||||||
@ -184,34 +194,41 @@ class APIEntityStateView(HomeAssistantView):
|
|||||||
else:
|
else:
|
||||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def post(self, request, entity_id):
|
def post(self, request, entity_id):
|
||||||
"""Update state of entity."""
|
"""Update state of entity."""
|
||||||
try:
|
try:
|
||||||
new_state = request.json['state']
|
data = yield from request.json()
|
||||||
except KeyError:
|
except ValueError:
|
||||||
|
return self.json_message('Invalid JSON specified',
|
||||||
|
HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
|
new_state = data.get('state')
|
||||||
|
|
||||||
|
if not new_state:
|
||||||
return self.json_message('No state specified', HTTP_BAD_REQUEST)
|
return self.json_message('No state specified', HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
attributes = request.json.get('attributes')
|
attributes = data.get('attributes')
|
||||||
force_update = request.json.get('force_update', False)
|
force_update = data.get('force_update', False)
|
||||||
|
|
||||||
is_new_state = self.hass.states.get(entity_id) is None
|
is_new_state = self.hass.states.get(entity_id) is None
|
||||||
|
|
||||||
# Write state
|
# Write state
|
||||||
self.hass.states.set(entity_id, new_state, attributes, force_update)
|
self.hass.states.async_set(entity_id, new_state, attributes,
|
||||||
|
force_update)
|
||||||
|
|
||||||
# Read the state back for our response
|
# Read the state back for our response
|
||||||
resp = self.json(self.hass.states.get(entity_id))
|
status_code = HTTP_CREATED if is_new_state else 200
|
||||||
|
resp = self.json(self.hass.states.get(entity_id), status_code)
|
||||||
if is_new_state:
|
|
||||||
resp.status_code = HTTP_CREATED
|
|
||||||
|
|
||||||
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
|
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
def delete(self, request, entity_id):
|
def delete(self, request, entity_id):
|
||||||
"""Remove entity."""
|
"""Remove entity."""
|
||||||
if self.hass.states.remove(entity_id):
|
if self.hass.states.async_remove(entity_id):
|
||||||
return self.json_message('Entity removed')
|
return self.json_message('Entity removed')
|
||||||
else:
|
else:
|
||||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||||
@ -223,20 +240,23 @@ class APIEventListenersView(HomeAssistantView):
|
|||||||
url = URL_API_EVENTS
|
url = URL_API_EVENTS
|
||||||
name = "api:event-listeners"
|
name = "api:event-listeners"
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get event listeners."""
|
"""Get event listeners."""
|
||||||
return self.json(events_json(self.hass))
|
return self.json(async_events_json(self.hass))
|
||||||
|
|
||||||
|
|
||||||
class APIEventView(HomeAssistantView):
|
class APIEventView(HomeAssistantView):
|
||||||
"""View to handle Event requests."""
|
"""View to handle Event requests."""
|
||||||
|
|
||||||
url = '/api/events/<event_type>'
|
url = '/api/events/{event_type}'
|
||||||
name = "api:event"
|
name = "api:event"
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def post(self, request, event_type):
|
def post(self, request, event_type):
|
||||||
"""Fire events."""
|
"""Fire events."""
|
||||||
event_data = request.json
|
body = yield from request.text()
|
||||||
|
event_data = json.loads(body) if body else None
|
||||||
|
|
||||||
if event_data is not None and not isinstance(event_data, dict):
|
if event_data is not None and not isinstance(event_data, dict):
|
||||||
return self.json_message('Event data should be a JSON object',
|
return self.json_message('Event data should be a JSON object',
|
||||||
@ -251,7 +271,7 @@ class APIEventView(HomeAssistantView):
|
|||||||
if state:
|
if state:
|
||||||
event_data[key] = state
|
event_data[key] = state
|
||||||
|
|
||||||
self.hass.bus.fire(event_type, event_data, ha.EventOrigin.remote)
|
self.hass.bus.async_fire(event_type, event_data, ha.EventOrigin.remote)
|
||||||
|
|
||||||
return self.json_message("Event {} fired.".format(event_type))
|
return self.json_message("Event {} fired.".format(event_type))
|
||||||
|
|
||||||
@ -262,24 +282,30 @@ class APIServicesView(HomeAssistantView):
|
|||||||
url = URL_API_SERVICES
|
url = URL_API_SERVICES
|
||||||
name = "api:services"
|
name = "api:services"
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get registered services."""
|
"""Get registered services."""
|
||||||
return self.json(services_json(self.hass))
|
return self.json(async_services_json(self.hass))
|
||||||
|
|
||||||
|
|
||||||
class APIDomainServicesView(HomeAssistantView):
|
class APIDomainServicesView(HomeAssistantView):
|
||||||
"""View to handle DomainServices requests."""
|
"""View to handle DomainServices requests."""
|
||||||
|
|
||||||
url = "/api/services/<domain>/<service>"
|
url = "/api/services/{domain}/{service}"
|
||||||
name = "api:domain-services"
|
name = "api:domain-services"
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def post(self, request, domain, service):
|
def post(self, request, domain, service):
|
||||||
"""Call a service.
|
"""Call a service.
|
||||||
|
|
||||||
Returns a list of changed states.
|
Returns a list of changed states.
|
||||||
"""
|
"""
|
||||||
with TrackStates(self.hass) as changed_states:
|
body = yield from request.text()
|
||||||
self.hass.services.call(domain, service, request.json, True)
|
data = json.loads(body) if body else None
|
||||||
|
|
||||||
|
with AsyncTrackStates(self.hass) as changed_states:
|
||||||
|
yield from self.hass.services.async_call(domain, service, data,
|
||||||
|
True)
|
||||||
|
|
||||||
return self.json(changed_states)
|
return self.json(changed_states)
|
||||||
|
|
||||||
@ -291,11 +317,14 @@ class APIEventForwardingView(HomeAssistantView):
|
|||||||
name = "api:event-forward"
|
name = "api:event-forward"
|
||||||
event_forwarder = None
|
event_forwarder = None
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Setup an event forwarder."""
|
"""Setup an event forwarder."""
|
||||||
data = request.json
|
try:
|
||||||
if data is None:
|
data = yield from request.json()
|
||||||
|
except ValueError:
|
||||||
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
host = data['host']
|
host = data['host']
|
||||||
api_password = data['api_password']
|
api_password = data['api_password']
|
||||||
@ -311,21 +340,25 @@ class APIEventForwardingView(HomeAssistantView):
|
|||||||
|
|
||||||
api = rem.API(host, api_password, port)
|
api = rem.API(host, api_password, port)
|
||||||
|
|
||||||
if not api.validate_api():
|
valid = yield from self.hass.loop.run_in_executor(
|
||||||
|
None, api.validate_api)
|
||||||
|
if not valid:
|
||||||
return self.json_message("Unable to validate API.",
|
return self.json_message("Unable to validate API.",
|
||||||
HTTP_UNPROCESSABLE_ENTITY)
|
HTTP_UNPROCESSABLE_ENTITY)
|
||||||
|
|
||||||
if self.event_forwarder is None:
|
if self.event_forwarder is None:
|
||||||
self.event_forwarder = rem.EventForwarder(self.hass)
|
self.event_forwarder = rem.EventForwarder(self.hass)
|
||||||
|
|
||||||
self.event_forwarder.connect(api)
|
self.event_forwarder.async_connect(api)
|
||||||
|
|
||||||
return self.json_message("Event forwarding setup.")
|
return self.json_message("Event forwarding setup.")
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
"""Remove event forwarer."""
|
"""Remove event forwarder."""
|
||||||
data = request.json
|
try:
|
||||||
if data is None:
|
data = yield from request.json()
|
||||||
|
except ValueError:
|
||||||
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -342,7 +375,7 @@ class APIEventForwardingView(HomeAssistantView):
|
|||||||
if self.event_forwarder is not None:
|
if self.event_forwarder is not None:
|
||||||
api = rem.API(host, None, port)
|
api = rem.API(host, None, port)
|
||||||
|
|
||||||
self.event_forwarder.disconnect(api)
|
self.event_forwarder.async_disconnect(api)
|
||||||
|
|
||||||
return self.json_message("Event forwarding cancelled.")
|
return self.json_message("Event forwarding cancelled.")
|
||||||
|
|
||||||
@ -353,6 +386,7 @@ class APIComponentsView(HomeAssistantView):
|
|||||||
url = URL_API_COMPONENTS
|
url = URL_API_COMPONENTS
|
||||||
name = "api:components"
|
name = "api:components"
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get current loaded components."""
|
"""Get current loaded components."""
|
||||||
return self.json(self.hass.config.components)
|
return self.json(self.hass.config.components)
|
||||||
@ -364,9 +398,12 @@ class APIErrorLogView(HomeAssistantView):
|
|||||||
url = URL_API_ERROR_LOG
|
url = URL_API_ERROR_LOG
|
||||||
name = "api:error-log"
|
name = "api:error-log"
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Serve error log."""
|
"""Serve error log."""
|
||||||
return self.file(request, self.hass.config.path(ERROR_LOG_FILENAME))
|
resp = yield from self.file(
|
||||||
|
request, self.hass.config.path(ERROR_LOG_FILENAME))
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
class APITemplateView(HomeAssistantView):
|
class APITemplateView(HomeAssistantView):
|
||||||
@ -375,23 +412,25 @@ class APITemplateView(HomeAssistantView):
|
|||||||
url = URL_API_TEMPLATE
|
url = URL_API_TEMPLATE
|
||||||
name = "api:template"
|
name = "api:template"
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Render a template."""
|
"""Render a template."""
|
||||||
try:
|
try:
|
||||||
tpl = template.Template(request.json['template'], self.hass)
|
data = yield from request.json()
|
||||||
return tpl.render(request.json.get('variables'))
|
tpl = template.Template(data['template'], self.hass)
|
||||||
except TemplateError as ex:
|
return tpl.async_render(data.get('variables'))
|
||||||
|
except (ValueError, TemplateError) as ex:
|
||||||
return self.json_message('Error rendering template: {}'.format(ex),
|
return self.json_message('Error rendering template: {}'.format(ex),
|
||||||
HTTP_BAD_REQUEST)
|
HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
def services_json(hass):
|
def async_services_json(hass):
|
||||||
"""Generate services data to JSONify."""
|
"""Generate services data to JSONify."""
|
||||||
return [{"domain": key, "services": value}
|
return [{"domain": key, "services": value}
|
||||||
for key, value in hass.services.services.items()]
|
for key, value in hass.services.async_services().items()]
|
||||||
|
|
||||||
|
|
||||||
def events_json(hass):
|
def async_events_json(hass):
|
||||||
"""Generate event data to JSONify."""
|
"""Generate event data to JSONify."""
|
||||||
return [{"event": key, "listener_count": value}
|
return [{"event": key, "listener_count": value}
|
||||||
for key, value in hass.bus.listeners.items()]
|
for key, value in hass.bus.async_listeners().items()]
|
||||||
|
@ -11,8 +11,7 @@ import os
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.bootstrap import async_prepare_setup_platform
|
||||||
from homeassistant.bootstrap import prepare_setup_platform
|
|
||||||
from homeassistant import config as conf_util
|
from homeassistant import config as conf_util
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||||
@ -25,7 +24,6 @@ from homeassistant.helpers.entity_component import EntityComponent
|
|||||||
from homeassistant.loader import get_platform
|
from homeassistant.loader import get_platform
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util.async import run_coroutine_threadsafe
|
|
||||||
|
|
||||||
DOMAIN = 'automation'
|
DOMAIN = 'automation'
|
||||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
@ -144,42 +142,50 @@ def reload(hass):
|
|||||||
hass.services.call(DOMAIN, SERVICE_RELOAD)
|
hass.services.call(DOMAIN, SERVICE_RELOAD)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
@asyncio.coroutine
|
||||||
|
def async_setup(hass, config):
|
||||||
"""Setup the automation."""
|
"""Setup the automation."""
|
||||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||||
group_name=GROUP_NAME_ALL_AUTOMATIONS)
|
group_name=GROUP_NAME_ALL_AUTOMATIONS)
|
||||||
|
|
||||||
success = run_coroutine_threadsafe(
|
success = yield from _async_process_config(hass, config, component)
|
||||||
_async_process_config(hass, config, component), hass.loop).result()
|
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
descriptions = conf_util.load_yaml_config_file(
|
descriptions = yield from hass.loop.run_in_executor(
|
||||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
None, conf_util.load_yaml_config_file, os.path.join(
|
||||||
|
os.path.dirname(__file__), 'services.yaml')
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@asyncio.coroutine
|
||||||
def trigger_service_handler(service_call):
|
def trigger_service_handler(service_call):
|
||||||
"""Handle automation triggers."""
|
"""Handle automation triggers."""
|
||||||
|
tasks = []
|
||||||
for entity in component.async_extract_from_service(service_call):
|
for entity in component.async_extract_from_service(service_call):
|
||||||
hass.loop.create_task(entity.async_trigger(
|
tasks.append(entity.async_trigger(
|
||||||
service_call.data.get(ATTR_VARIABLES), True))
|
service_call.data.get(ATTR_VARIABLES), True))
|
||||||
|
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||||
|
|
||||||
@callback
|
@asyncio.coroutine
|
||||||
def turn_onoff_service_handler(service_call):
|
def turn_onoff_service_handler(service_call):
|
||||||
"""Handle automation turn on/off service calls."""
|
"""Handle automation turn on/off service calls."""
|
||||||
|
tasks = []
|
||||||
method = 'async_{}'.format(service_call.service)
|
method = 'async_{}'.format(service_call.service)
|
||||||
for entity in component.async_extract_from_service(service_call):
|
for entity in component.async_extract_from_service(service_call):
|
||||||
hass.loop.create_task(getattr(entity, method)())
|
tasks.append(getattr(entity, method)())
|
||||||
|
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||||
|
|
||||||
@callback
|
@asyncio.coroutine
|
||||||
def toggle_service_handler(service_call):
|
def toggle_service_handler(service_call):
|
||||||
"""Handle automation toggle service calls."""
|
"""Handle automation toggle service calls."""
|
||||||
|
tasks = []
|
||||||
for entity in component.async_extract_from_service(service_call):
|
for entity in component.async_extract_from_service(service_call):
|
||||||
if entity.is_on:
|
if entity.is_on:
|
||||||
hass.loop.create_task(entity.async_turn_off())
|
tasks.append(entity.async_turn_off())
|
||||||
else:
|
else:
|
||||||
hass.loop.create_task(entity.async_turn_on())
|
tasks.append(entity.async_turn_on())
|
||||||
|
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def reload_service_handler(service_call):
|
def reload_service_handler(service_call):
|
||||||
@ -187,24 +193,24 @@ def setup(hass, config):
|
|||||||
conf = yield from component.async_prepare_reload()
|
conf = yield from component.async_prepare_reload()
|
||||||
if conf is None:
|
if conf is None:
|
||||||
return
|
return
|
||||||
hass.loop.create_task(_async_process_config(hass, conf, component))
|
yield from _async_process_config(hass, conf, component)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
hass.services.async_register(
|
||||||
descriptions.get(SERVICE_TRIGGER),
|
DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
||||||
schema=TRIGGER_SERVICE_SCHEMA)
|
descriptions.get(SERVICE_TRIGGER), schema=TRIGGER_SERVICE_SCHEMA)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
hass.services.async_register(
|
||||||
descriptions.get(SERVICE_RELOAD),
|
DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||||
schema=RELOAD_SERVICE_SCHEMA)
|
descriptions.get(SERVICE_RELOAD), schema=RELOAD_SERVICE_SCHEMA)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
|
hass.services.async_register(
|
||||||
descriptions.get(SERVICE_TOGGLE),
|
DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
|
||||||
schema=SERVICE_SCHEMA)
|
descriptions.get(SERVICE_TOGGLE), schema=SERVICE_SCHEMA)
|
||||||
|
|
||||||
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
|
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
|
||||||
hass.services.register(DOMAIN, service, turn_onoff_service_handler,
|
hass.services.async_register(
|
||||||
descriptions.get(service),
|
DOMAIN, service, turn_onoff_service_handler,
|
||||||
schema=SERVICE_SCHEMA)
|
descriptions.get(service), schema=SERVICE_SCHEMA)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -212,8 +218,6 @@ def setup(hass, config):
|
|||||||
class AutomationEntity(ToggleEntity):
|
class AutomationEntity(ToggleEntity):
|
||||||
"""Entity to show status of entity."""
|
"""Entity to show status of entity."""
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
|
||||||
def __init__(self, name, async_attach_triggers, cond_func, async_action,
|
def __init__(self, name, async_attach_triggers, cond_func, async_action,
|
||||||
hidden):
|
hidden):
|
||||||
"""Initialize an automation entity."""
|
"""Initialize an automation entity."""
|
||||||
@ -260,7 +264,7 @@ class AutomationEntity(ToggleEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
yield from self.async_enable()
|
yield from self.async_enable()
|
||||||
self.hass.loop.create_task(self.async_update_ha_state())
|
yield from self.async_update_ha_state()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_turn_off(self, **kwargs) -> None:
|
def async_turn_off(self, **kwargs) -> None:
|
||||||
@ -271,8 +275,6 @@ class AutomationEntity(ToggleEntity):
|
|||||||
self._async_detach_triggers()
|
self._async_detach_triggers()
|
||||||
self._async_detach_triggers = None
|
self._async_detach_triggers = None
|
||||||
self._enabled = False
|
self._enabled = False
|
||||||
# It's important that the update is finished before this method
|
|
||||||
# ends because async_remove depends on it.
|
|
||||||
yield from self.async_update_ha_state()
|
yield from self.async_update_ha_state()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
@ -284,7 +286,7 @@ class AutomationEntity(ToggleEntity):
|
|||||||
if skip_condition or self._cond_func(variables):
|
if skip_condition or self._cond_func(variables):
|
||||||
yield from self._async_action(self.entity_id, variables)
|
yield from self._async_action(self.entity_id, variables)
|
||||||
self._last_triggered = utcnow()
|
self._last_triggered = utcnow()
|
||||||
self.hass.loop.create_task(self.async_update_ha_state())
|
yield from self.async_update_ha_state()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_remove(self):
|
def async_remove(self):
|
||||||
@ -347,7 +349,7 @@ def _async_process_config(hass, config, component):
|
|||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
|
|
||||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||||
hass.loop.create_task(component.async_add_entities(entities))
|
yield from component.async_add_entities(entities)
|
||||||
|
|
||||||
return len(entities) > 0
|
return len(entities) > 0
|
||||||
|
|
||||||
@ -362,7 +364,7 @@ def _async_get_action(hass, config, name):
|
|||||||
_LOGGER.info('Executing %s', name)
|
_LOGGER.info('Executing %s', name)
|
||||||
logbook.async_log_entry(
|
logbook.async_log_entry(
|
||||||
hass, name, 'has been triggered', DOMAIN, entity_id)
|
hass, name, 'has been triggered', DOMAIN, entity_id)
|
||||||
hass.loop.create_task(script_obj.async_run(variables))
|
yield from script_obj.async_run(variables)
|
||||||
|
|
||||||
return action
|
return action
|
||||||
|
|
||||||
@ -395,9 +397,8 @@ def _async_process_trigger(hass, config, trigger_configs, name, action):
|
|||||||
removes = []
|
removes = []
|
||||||
|
|
||||||
for conf in trigger_configs:
|
for conf in trigger_configs:
|
||||||
platform = yield from hass.loop.run_in_executor(
|
platform = yield from async_prepare_setup_platform(
|
||||||
None, prepare_setup_platform, hass, config, DOMAIN,
|
hass, config, DOMAIN, conf.get(CONF_PLATFORM))
|
||||||
conf.get(CONF_PLATFORM))
|
|
||||||
|
|
||||||
if platform is None:
|
if platform is None:
|
||||||
return None
|
return None
|
||||||
|
41
homeassistant/components/automation/litejet.py
Normal file
41
homeassistant/components/automation/litejet.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
Trigger an automation when a LiteJet switch is released.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/automation.litejet/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.const import CONF_PLATFORM
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
DEPENDENCIES = ['litejet']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_NUMBER = 'number'
|
||||||
|
|
||||||
|
TRIGGER_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(CONF_PLATFORM): 'litejet',
|
||||||
|
vol.Required(CONF_NUMBER): cv.positive_int
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def async_trigger(hass, config, action):
|
||||||
|
"""Listen for events based on configuration."""
|
||||||
|
number = config.get(CONF_NUMBER)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def call_action():
|
||||||
|
"""Call action with right context."""
|
||||||
|
hass.async_run_job(action, {
|
||||||
|
'trigger': {
|
||||||
|
CONF_PLATFORM: 'litejet',
|
||||||
|
CONF_NUMBER: number
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
hass.data['litejet_system'].on_switch_released(number, call_action)
|
@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
sensor_class, pin)])
|
sensor_class, pin)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
|
||||||
class ArestBinarySensor(BinarySensorDevice):
|
class ArestBinarySensor(BinarySensorDevice):
|
||||||
"""Implement an aREST binary sensor for a pin."""
|
"""Implement an aREST binary sensor for a pin."""
|
||||||
|
|
||||||
@ -93,7 +92,6 @@ class ArestBinarySensor(BinarySensorDevice):
|
|||||||
self.arest.update()
|
self.arest.update()
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
|
||||||
class ArestData(object):
|
class ArestData(object):
|
||||||
"""Class for handling the data retrieval for pins."""
|
"""Class for handling the data retrieval for pins."""
|
||||||
|
|
||||||
|
@ -53,7 +53,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
value_template)])
|
value_template)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
|
||||||
class CommandBinarySensor(BinarySensorDevice):
|
class CommandBinarySensor(BinarySensorDevice):
|
||||||
"""Represent a command line binary sensor."""
|
"""Represent a command line binary sensor."""
|
||||||
|
|
||||||
|
@ -5,20 +5,16 @@ For more details about this platform, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/binary_sensor.concord232/
|
https://home-assistant.io/components/binary_sensor.concord232/
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES)
|
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES)
|
||||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
|
|
||||||
REQUIREMENTS = ['concord232==0.14']
|
REQUIREMENTS = ['concord232==0.14']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -27,9 +23,12 @@ CONF_EXCLUDE_ZONES = 'exclude_zones'
|
|||||||
CONF_ZONE_TYPES = 'zone_types'
|
CONF_ZONE_TYPES = 'zone_types'
|
||||||
|
|
||||||
DEFAULT_HOST = 'localhost'
|
DEFAULT_HOST = 'localhost'
|
||||||
|
DEFAULT_NAME = 'Alarm'
|
||||||
DEFAULT_PORT = '5007'
|
DEFAULT_PORT = '5007'
|
||||||
DEFAULT_SSL = False
|
DEFAULT_SSL = False
|
||||||
|
|
||||||
|
SCAN_INTERVAL = 1
|
||||||
|
|
||||||
ZONE_TYPES_SCHEMA = vol.Schema({
|
ZONE_TYPES_SCHEMA = vol.Schema({
|
||||||
cv.positive_int: vol.In(SENSOR_CLASSES),
|
cv.positive_int: vol.In(SENSOR_CLASSES),
|
||||||
})
|
})
|
||||||
@ -42,12 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA,
|
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA,
|
||||||
})
|
})
|
||||||
|
|
||||||
SCAN_INTERVAL = 1
|
|
||||||
|
|
||||||
DEFAULT_NAME = "Alarm"
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-locals
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the Concord232 binary sensor platform."""
|
"""Set up the Concord232 binary sensor platform."""
|
||||||
from concord232 import client as concord232_client
|
from concord232 import client as concord232_client
|
||||||
@ -59,24 +53,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
sensors = []
|
sensors = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.debug('Initializing Client.')
|
_LOGGER.debug("Initializing Client")
|
||||||
client = concord232_client.Client('http://{}:{}'
|
client = concord232_client.Client('http://{}:{}'.format(host, port))
|
||||||
.format(host, port))
|
|
||||||
client.zones = client.list_zones()
|
client.zones = client.list_zones()
|
||||||
client.last_zone_update = datetime.datetime.now()
|
client.last_zone_update = datetime.datetime.now()
|
||||||
|
|
||||||
except requests.exceptions.ConnectionError as ex:
|
except requests.exceptions.ConnectionError as ex:
|
||||||
_LOGGER.error('Unable to connect to Concord232: %s', str(ex))
|
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for zone in client.zones:
|
for zone in client.zones:
|
||||||
_LOGGER.info('Loading Zone found: %s', zone['name'])
|
_LOGGER.info("Loading Zone found: %s", zone['name'])
|
||||||
if zone['number'] not in exclude:
|
if zone['number'] not in exclude:
|
||||||
sensors.append(Concord232ZoneSensor(
|
sensors.append(
|
||||||
hass,
|
Concord232ZoneSensor(
|
||||||
client,
|
hass, client, zone, zone_types.get(zone['number'],
|
||||||
zone,
|
get_opening_type(zone)))
|
||||||
zone_types.get(zone['number'], get_opening_type(zone))))
|
)
|
||||||
|
|
||||||
add_devices(sensors)
|
add_devices(sensors)
|
||||||
|
|
||||||
@ -84,16 +77,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
|
|
||||||
def get_opening_type(zone):
|
def get_opening_type(zone):
|
||||||
"""Helper function to try to guess sensor type frm name."""
|
"""Helper function to try to guess sensor type from name."""
|
||||||
if "MOTION" in zone["name"]:
|
if 'MOTION' in zone['name']:
|
||||||
return "motion"
|
return 'motion'
|
||||||
if "KEY" in zone["name"]:
|
if 'KEY' in zone['name']:
|
||||||
return "safety"
|
return 'safety'
|
||||||
if "SMOKE" in zone["name"]:
|
if 'SMOKE' in zone['name']:
|
||||||
return "smoke"
|
return 'smoke'
|
||||||
if "WATER" in zone["name"]:
|
if 'WATER' in zone['name']:
|
||||||
return "water"
|
return 'water'
|
||||||
return "opening"
|
return 'opening'
|
||||||
|
|
||||||
|
|
||||||
class Concord232ZoneSensor(BinarySensorDevice):
|
class Concord232ZoneSensor(BinarySensorDevice):
|
||||||
|
@ -35,7 +35,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||||
"""Representation of an Envisalink binary sensor."""
|
"""Representation of an Envisalink binary sensor."""
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def __init__(self, zone_number, zone_name, zone_type, info, controller):
|
def __init__(self, zone_number, zone_name, zone_type, info, controller):
|
||||||
"""Initialize the binary_sensor."""
|
"""Initialize the binary_sensor."""
|
||||||
from pydispatch import dispatcher
|
from pydispatch import dispatcher
|
||||||
|
@ -81,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
from haffmpeg import SensorNoise, SensorMotion
|
from haffmpeg import SensorNoise, SensorMotion
|
||||||
|
|
||||||
# check source
|
# check source
|
||||||
if not run_test(config.get(CONF_INPUT)):
|
if not run_test(hass, config.get(CONF_INPUT)):
|
||||||
return
|
return
|
||||||
|
|
||||||
# generate sensor object
|
# generate sensor object
|
||||||
|
@ -51,7 +51,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
)])
|
)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
|
||||||
class MqttBinarySensor(BinarySensorDevice):
|
class MqttBinarySensor(BinarySensorDevice):
|
||||||
"""Representation a binary sensor that is updated by MQTT."""
|
"""Representation a binary sensor that is updated by MQTT."""
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import (
|
|||||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.components.sensor.nest import NestSensor
|
from homeassistant.components.sensor.nest import NestSensor
|
||||||
from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS)
|
from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS)
|
||||||
import homeassistant.components.nest as nest
|
from homeassistant.components.nest import DATA_NEST
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DEPENDENCIES = ['nest']
|
DEPENDENCIES = ['nest']
|
||||||
@ -35,10 +35,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup Nest binary sensors."""
|
"""Setup Nest binary sensors."""
|
||||||
|
nest = hass.data[DATA_NEST]
|
||||||
|
|
||||||
|
all_sensors = []
|
||||||
for structure, device in nest.devices():
|
for structure, device in nest.devices():
|
||||||
add_devices([NestBinarySensor(structure, device, variable)
|
all_sensors.extend(
|
||||||
|
[NestBinarySensor(structure, device, variable)
|
||||||
for variable in config[CONF_MONITORED_CONDITIONS]])
|
for variable in config[CONF_MONITORED_CONDITIONS]])
|
||||||
|
|
||||||
|
add_devices(all_sensors, True)
|
||||||
|
|
||||||
|
|
||||||
class NestBinarySensor(NestSensor, BinarySensorDevice):
|
class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||||
"""Represents a Nest binary sensor."""
|
"""Represents a Nest binary sensor."""
|
||||||
@ -46,4 +52,8 @@ class NestBinarySensor(NestSensor, BinarySensorDevice):
|
|||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""True if the binary sensor is on."""
|
"""True if the binary sensor is on."""
|
||||||
return bool(getattr(self.device, self.variable))
|
return self._state
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Retrieve latest state."""
|
||||||
|
self._state = bool(getattr(self.device, self.variable))
|
||||||
|
@ -58,11 +58,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
add_devices(devices)
|
add_devices(devices)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
class OctoPrintBinarySensor(BinarySensorDevice):
|
class OctoPrintBinarySensor(BinarySensorDevice):
|
||||||
"""Representation an OctoPrint binary sensor."""
|
"""Representation an OctoPrint binary sensor."""
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def __init__(self, api, condition, sensor_type, sensor_name, unit,
|
def __init__(self, api, condition, sensor_type, sensor_name, unit,
|
||||||
endpoint, group, tool=None):
|
endpoint, group, tool=None):
|
||||||
"""Initialize a new OctoPrint sensor."""
|
"""Initialize a new OctoPrint sensor."""
|
||||||
|
@ -41,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-variable, too-many-locals
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the REST binary sensor."""
|
"""Setup the REST binary sensor."""
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
@ -76,7 +75,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
hass, rest, name, sensor_class, value_template)])
|
hass, rest, name, sensor_class, value_template)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
class RestBinarySensor(BinarySensorDevice):
|
class RestBinarySensor(BinarySensorDevice):
|
||||||
"""Representation of a REST binary sensor."""
|
"""Representation of a REST binary sensor."""
|
||||||
|
|
||||||
|
@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
add_devices(binary_sensors)
|
add_devices(binary_sensors)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
|
||||||
class RPiGPIOBinarySensor(BinarySensorDevice):
|
class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||||
"""Represent a binary sensor that uses Raspberry Pi GPIO."""
|
"""Represent a binary sensor that uses Raspberry Pi GPIO."""
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
add_devices(dev)
|
add_devices(dev)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice):
|
class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice):
|
||||||
"""Implementation of a SleepIQ presence sensor."""
|
"""Implementation of a SleepIQ presence sensor."""
|
||||||
|
|
||||||
|
@ -63,14 +63,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
_LOGGER.error('No sensors added')
|
_LOGGER.error('No sensors added')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
hass.loop.create_task(async_add_devices(sensors))
|
hass.loop.create_task(async_add_devices(sensors, True))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class BinarySensorTemplate(BinarySensorDevice):
|
class BinarySensorTemplate(BinarySensorDevice):
|
||||||
"""A virtual binary sensor that triggers from another sensor."""
|
"""A virtual binary sensor that triggers from another sensor."""
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def __init__(self, hass, device, friendly_name, sensor_class,
|
def __init__(self, hass, device, friendly_name, sensor_class,
|
||||||
value_template, entity_ids):
|
value_template, entity_ids):
|
||||||
"""Initialize the Template binary sensor."""
|
"""Initialize the Template binary sensor."""
|
||||||
@ -82,8 +81,6 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||||||
self._template = value_template
|
self._template = value_template
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
self._async_render()
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||||
"""Called when the target device changes state."""
|
"""Called when the target device changes state."""
|
||||||
@ -115,10 +112,6 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_update(self):
|
def async_update(self):
|
||||||
"""Update the state from the template."""
|
"""Update the state from the template."""
|
||||||
self._async_render()
|
|
||||||
|
|
||||||
def _async_render(self):
|
|
||||||
"""Render the state from the template."""
|
|
||||||
try:
|
try:
|
||||||
self._state = self._template.async_render().lower() == 'true'
|
self._state = self._template.async_render().lower() == 'true'
|
||||||
except TemplateError as ex:
|
except TemplateError as ex:
|
||||||
|
@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
class SensorTrend(BinarySensorDevice):
|
class SensorTrend(BinarySensorDevice):
|
||||||
"""Representation of a trend Sensor."""
|
"""Representation of a trend Sensor."""
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
|
||||||
def __init__(self, hass, device_id, friendly_name,
|
def __init__(self, hass, device_id, friendly_name,
|
||||||
target_entity, attribute, sensor_class, invert):
|
target_entity, attribute, sensor_class, invert):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
|
@ -16,9 +16,9 @@ DEPENDENCIES = ['vera']
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Perform the setup for Vera controller devices."""
|
"""Perform the setup for Vera controller devices."""
|
||||||
add_devices_callback(
|
add_devices(
|
||||||
VeraBinarySensor(device, VERA_CONTROLLER)
|
VeraBinarySensor(device, VERA_CONTROLLER)
|
||||||
for device in VERA_DEVICES['binary_sensor'])
|
for device in VERA_DEVICES['binary_sensor'])
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument,too-few-public-methods
|
# pylint: disable=unused-argument
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup BloomSky component."""
|
"""Setup BloomSky component."""
|
||||||
api_key = config[DOMAIN][CONF_API_KEY]
|
api_key = config[DOMAIN][CONF_API_KEY]
|
||||||
|
@ -5,8 +5,10 @@ Component to interface with cameras.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/camera/
|
https://home-assistant.io/components/camera/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
@ -25,17 +27,16 @@ STATE_IDLE = 'idle'
|
|||||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
@asyncio.coroutine
|
||||||
def setup(hass, config):
|
def async_setup(hass, config):
|
||||||
"""Setup the camera component."""
|
"""Setup the camera component."""
|
||||||
component = EntityComponent(
|
component = EntityComponent(
|
||||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||||
|
|
||||||
hass.wsgi.register_view(CameraImageView(hass, component.entities))
|
hass.http.register_view(CameraImageView(hass, component.entities))
|
||||||
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities))
|
hass.http.register_view(CameraMjpegStream(hass, component.entities))
|
||||||
|
|
||||||
component.setup(config)
|
|
||||||
|
|
||||||
|
yield from component.async_setup(config)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -80,33 +81,59 @@ class Camera(Entity):
|
|||||||
"""Return bytes of camera image."""
|
"""Return bytes of camera image."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def mjpeg_stream(self, response):
|
@asyncio.coroutine
|
||||||
"""Generate an HTTP MJPEG stream from camera images."""
|
def async_camera_image(self):
|
||||||
def stream():
|
"""Return bytes of camera image.
|
||||||
"""Stream images as mjpeg stream."""
|
|
||||||
try:
|
|
||||||
last_image = None
|
|
||||||
while True:
|
|
||||||
img_bytes = self.camera_image()
|
|
||||||
|
|
||||||
if img_bytes is not None and img_bytes != last_image:
|
This method must be run in the event loop.
|
||||||
yield bytes(
|
"""
|
||||||
|
image = yield from self.hass.loop.run_in_executor(
|
||||||
|
None, self.camera_image)
|
||||||
|
return image
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def handle_async_mjpeg_stream(self, request):
|
||||||
|
"""Generate an HTTP MJPEG stream from camera images.
|
||||||
|
|
||||||
|
This method must be run in the event loop.
|
||||||
|
"""
|
||||||
|
response = web.StreamResponse()
|
||||||
|
|
||||||
|
response.content_type = ('multipart/x-mixed-replace; '
|
||||||
|
'boundary=--jpegboundary')
|
||||||
|
response.enable_chunked_encoding()
|
||||||
|
yield from response.prepare(request)
|
||||||
|
|
||||||
|
def write(img_bytes):
|
||||||
|
"""Write image to stream."""
|
||||||
|
response.write(bytes(
|
||||||
'--jpegboundary\r\n'
|
'--jpegboundary\r\n'
|
||||||
'Content-Type: image/jpeg\r\n'
|
'Content-Type: image/jpeg\r\n'
|
||||||
'Content-Length: {}\r\n\r\n'.format(
|
'Content-Length: {}\r\n\r\n'.format(
|
||||||
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n'
|
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n')
|
||||||
|
|
||||||
|
last_image = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
img_bytes = yield from self.async_camera_image()
|
||||||
|
if not img_bytes:
|
||||||
|
break
|
||||||
|
|
||||||
|
if img_bytes is not None and img_bytes != last_image:
|
||||||
|
write(img_bytes)
|
||||||
|
|
||||||
|
# Chrome seems to always ignore first picture,
|
||||||
|
# print it twice.
|
||||||
|
if last_image is None:
|
||||||
|
write(img_bytes)
|
||||||
|
|
||||||
last_image = img_bytes
|
last_image = img_bytes
|
||||||
|
yield from response.drain()
|
||||||
|
|
||||||
time.sleep(0.5)
|
yield from asyncio.sleep(.5)
|
||||||
except GeneratorExit:
|
finally:
|
||||||
pass
|
yield from response.write_eof()
|
||||||
|
|
||||||
return response(
|
|
||||||
stream(),
|
|
||||||
content_type=('multipart/x-mixed-replace; '
|
|
||||||
'boundary=--jpegboundary')
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
@ -144,22 +171,25 @@ class CameraView(HomeAssistantView):
|
|||||||
super().__init__(hass)
|
super().__init__(hass)
|
||||||
self.entities = entities
|
self.entities = entities
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def get(self, request, entity_id):
|
def get(self, request, entity_id):
|
||||||
"""Start a get request."""
|
"""Start a get request."""
|
||||||
camera = self.entities.get(entity_id)
|
camera = self.entities.get(entity_id)
|
||||||
|
|
||||||
if camera is None:
|
if camera is None:
|
||||||
return self.Response(status=404)
|
return web.Response(status=404)
|
||||||
|
|
||||||
authenticated = (request.authenticated or
|
authenticated = (request.authenticated or
|
||||||
request.args.get('token') == camera.access_token)
|
request.GET.get('token') == camera.access_token)
|
||||||
|
|
||||||
if not authenticated:
|
if not authenticated:
|
||||||
return self.Response(status=401)
|
return web.Response(status=401)
|
||||||
|
|
||||||
return self.handle(camera)
|
response = yield from self.handle(request, camera)
|
||||||
|
return response
|
||||||
|
|
||||||
def handle(self, camera):
|
@asyncio.coroutine
|
||||||
|
def handle(self, request, camera):
|
||||||
"""Hanlde the camera request."""
|
"""Hanlde the camera request."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@ -167,25 +197,27 @@ class CameraView(HomeAssistantView):
|
|||||||
class CameraImageView(CameraView):
|
class CameraImageView(CameraView):
|
||||||
"""Camera view to serve an image."""
|
"""Camera view to serve an image."""
|
||||||
|
|
||||||
url = "/api/camera_proxy/<entity(domain=camera):entity_id>"
|
url = "/api/camera_proxy/{entity_id}"
|
||||||
name = "api:camera:image"
|
name = "api:camera:image"
|
||||||
|
|
||||||
def handle(self, camera):
|
@asyncio.coroutine
|
||||||
|
def handle(self, request, camera):
|
||||||
"""Serve camera image."""
|
"""Serve camera image."""
|
||||||
response = camera.camera_image()
|
image = yield from camera.async_camera_image()
|
||||||
|
|
||||||
if response is None:
|
if image is None:
|
||||||
return self.Response(status=500)
|
return web.Response(status=500)
|
||||||
|
|
||||||
return self.Response(response)
|
return web.Response(body=image)
|
||||||
|
|
||||||
|
|
||||||
class CameraMjpegStream(CameraView):
|
class CameraMjpegStream(CameraView):
|
||||||
"""Camera View to serve an MJPEG stream."""
|
"""Camera View to serve an MJPEG stream."""
|
||||||
|
|
||||||
url = "/api/camera_proxy_stream/<entity(domain=camera):entity_id>"
|
url = "/api/camera_proxy_stream/{entity_id}"
|
||||||
name = "api:camera:stream"
|
name = "api:camera:stream"
|
||||||
|
|
||||||
def handle(self, camera):
|
@asyncio.coroutine
|
||||||
|
def handle(self, request, camera):
|
||||||
"""Serve camera image."""
|
"""Serve camera image."""
|
||||||
return camera.mjpeg_stream(self.Response)
|
yield from camera.handle_async_mjpeg_stream(request)
|
||||||
|
@ -4,15 +4,18 @@ Support for Cameras with FFmpeg as decoder.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/camera.ffmpeg/
|
https://home-assistant.io/components/camera.ffmpeg/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||||
from homeassistant.components.ffmpeg import (
|
from homeassistant.components.ffmpeg import (
|
||||||
run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
|
async_run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
|
from homeassistant.util.async import run_coroutine_threadsafe
|
||||||
|
|
||||||
DEPENDENCIES = ['ffmpeg']
|
DEPENDENCIES = ['ffmpeg']
|
||||||
|
|
||||||
@ -27,17 +30,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
@asyncio.coroutine
|
||||||
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Setup a FFmpeg Camera."""
|
"""Setup a FFmpeg Camera."""
|
||||||
if not run_test(config.get(CONF_INPUT)):
|
if not async_run_test(hass, config.get(CONF_INPUT)):
|
||||||
return
|
return
|
||||||
add_devices([FFmpegCamera(config)])
|
hass.loop.create_task(async_add_devices([FFmpegCamera(hass, config)]))
|
||||||
|
|
||||||
|
|
||||||
class FFmpegCamera(Camera):
|
class FFmpegCamera(Camera):
|
||||||
"""An implementation of an FFmpeg camera."""
|
"""An implementation of an FFmpeg camera."""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, hass, config):
|
||||||
"""Initialize a FFmpeg camera."""
|
"""Initialize a FFmpeg camera."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._name = config.get(CONF_NAME)
|
self._name = config.get(CONF_NAME)
|
||||||
@ -45,24 +49,45 @@ class FFmpegCamera(Camera):
|
|||||||
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
||||||
|
|
||||||
def camera_image(self):
|
def camera_image(self):
|
||||||
|
"""Return bytes of camera image."""
|
||||||
|
return run_coroutine_threadsafe(
|
||||||
|
self.async_camera_image(), self.hass.loop).result()
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_camera_image(self):
|
||||||
"""Return a still image response from the camera."""
|
"""Return a still image response from the camera."""
|
||||||
from haffmpeg import ImageSingle, IMAGE_JPEG
|
from haffmpeg import ImageSingleAsync, IMAGE_JPEG
|
||||||
ffmpeg = ImageSingle(get_binary())
|
ffmpeg = ImageSingleAsync(get_binary(), loop=self.hass.loop)
|
||||||
|
|
||||||
return ffmpeg.get_image(self._input, output_format=IMAGE_JPEG,
|
image = yield from ffmpeg.get_image(
|
||||||
|
self._input, output_format=IMAGE_JPEG,
|
||||||
extra_cmd=self._extra_arguments)
|
extra_cmd=self._extra_arguments)
|
||||||
|
return image
|
||||||
|
|
||||||
def mjpeg_stream(self, response):
|
@asyncio.coroutine
|
||||||
|
def handle_async_mjpeg_stream(self, request):
|
||||||
"""Generate an HTTP MJPEG stream from the camera."""
|
"""Generate an HTTP MJPEG stream from the camera."""
|
||||||
from haffmpeg import CameraMjpeg
|
from haffmpeg import CameraMjpegAsync
|
||||||
|
|
||||||
stream = CameraMjpeg(get_binary())
|
stream = CameraMjpegAsync(get_binary(), loop=self.hass.loop)
|
||||||
stream.open_camera(self._input, extra_cmd=self._extra_arguments)
|
yield from stream.open_camera(
|
||||||
return response(
|
self._input, extra_cmd=self._extra_arguments)
|
||||||
stream,
|
|
||||||
mimetype='multipart/x-mixed-replace;boundary=ffserver',
|
response = web.StreamResponse()
|
||||||
direct_passthrough=True
|
response.content_type = 'multipart/x-mixed-replace;boundary=ffserver'
|
||||||
)
|
response.enable_chunked_encoding()
|
||||||
|
|
||||||
|
yield from response.prepare(request)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = yield from stream.read(102400)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
response.write(data)
|
||||||
|
finally:
|
||||||
|
self.hass.loop.create_task(stream.close())
|
||||||
|
yield from response.write_eof()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -36,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
add_devices([FoscamCamera(config)])
|
add_devices([FoscamCamera(config)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
class FoscamCamera(Camera):
|
class FoscamCamera(Camera):
|
||||||
"""An implementation of a Foscam IP camera."""
|
"""An implementation of a Foscam IP camera."""
|
||||||
|
|
||||||
|
@ -4,10 +4,13 @@ Support for IP Cameras.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/camera.generic/
|
https://home-assistant.io/components/camera.generic/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import async_timeout
|
||||||
import requests
|
import requests
|
||||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
from requests.auth import HTTPDigestAuth
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -16,6 +19,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.util.async import run_coroutine_threadsafe
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -35,13 +39,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Setup a generic IP Camera."""
|
"""Setup a generic IP Camera."""
|
||||||
add_devices([GenericCamera(hass, config)])
|
hass.loop.create_task(async_add_devices([GenericCamera(hass, config)]))
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
class GenericCamera(Camera):
|
class GenericCamera(Camera):
|
||||||
"""A generic implementation of an IP camera."""
|
"""A generic implementation of an IP camera."""
|
||||||
|
|
||||||
@ -49,6 +53,7 @@ class GenericCamera(Camera):
|
|||||||
"""Initialize a generic camera."""
|
"""Initialize a generic camera."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
self._authentication = device_info.get(CONF_AUTHENTICATION)
|
||||||
self._name = device_info.get(CONF_NAME)
|
self._name = device_info.get(CONF_NAME)
|
||||||
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
|
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
|
||||||
self._still_image_url.hass = hass
|
self._still_image_url.hass = hass
|
||||||
@ -58,10 +63,10 @@ class GenericCamera(Camera):
|
|||||||
password = device_info.get(CONF_PASSWORD)
|
password = device_info.get(CONF_PASSWORD)
|
||||||
|
|
||||||
if username and password:
|
if username and password:
|
||||||
if device_info[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION:
|
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||||
self._auth = HTTPDigestAuth(username, password)
|
self._auth = HTTPDigestAuth(username, password)
|
||||||
else:
|
else:
|
||||||
self._auth = HTTPBasicAuth(username, password)
|
self._auth = aiohttp.BasicAuth(username, password=password)
|
||||||
else:
|
else:
|
||||||
self._auth = None
|
self._auth = None
|
||||||
|
|
||||||
@ -69,9 +74,15 @@ class GenericCamera(Camera):
|
|||||||
self._last_image = None
|
self._last_image = None
|
||||||
|
|
||||||
def camera_image(self):
|
def camera_image(self):
|
||||||
|
"""Return bytes of camera image."""
|
||||||
|
return run_coroutine_threadsafe(
|
||||||
|
self.async_camera_image(), self.hass.loop).result()
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_camera_image(self):
|
||||||
"""Return a still image response from the camera."""
|
"""Return a still image response from the camera."""
|
||||||
try:
|
try:
|
||||||
url = self._still_image_url.render()
|
url = self._still_image_url.async_render()
|
||||||
except TemplateError as err:
|
except TemplateError as err:
|
||||||
_LOGGER.error('Error parsing template %s: %s',
|
_LOGGER.error('Error parsing template %s: %s',
|
||||||
self._still_image_url, err)
|
self._still_image_url, err)
|
||||||
@ -80,16 +91,35 @@ class GenericCamera(Camera):
|
|||||||
if url == self._last_url and self._limit_refetch:
|
if url == self._last_url and self._limit_refetch:
|
||||||
return self._last_image
|
return self._last_image
|
||||||
|
|
||||||
kwargs = {'timeout': 10, 'auth': self._auth}
|
# aiohttp don't support DigestAuth jet
|
||||||
|
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||||
|
def fetch():
|
||||||
|
"""Read image from a URL."""
|
||||||
try:
|
try:
|
||||||
|
kwargs = {'timeout': 10, 'auth': self._auth}
|
||||||
response = requests.get(url, **kwargs)
|
response = requests.get(url, **kwargs)
|
||||||
|
return response.content
|
||||||
except requests.exceptions.RequestException as error:
|
except requests.exceptions.RequestException as error:
|
||||||
_LOGGER.error('Error getting camera image: %s', error)
|
_LOGGER.error('Error getting camera image: %s', error)
|
||||||
return None
|
return self._last_image
|
||||||
|
|
||||||
|
self._last_image = yield from self.hass.loop.run_in_executor(
|
||||||
|
None, fetch)
|
||||||
|
# async
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||||
|
respone = yield from self.hass.websession.get(
|
||||||
|
url,
|
||||||
|
auth=self._auth
|
||||||
|
)
|
||||||
|
self._last_image = yield from respone.read()
|
||||||
|
yield from respone.release()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.error('Timeout getting camera image')
|
||||||
|
return self._last_image
|
||||||
|
|
||||||
self._last_url = url
|
self._last_url = url
|
||||||
self._last_image = response.content
|
|
||||||
return self._last_image
|
return self._last_image
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -4,9 +4,14 @@ Support for IP Cameras.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/camera.mjpeg/
|
https://home-assistant.io/components/camera.mjpeg/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from aiohttp import web
|
||||||
|
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
||||||
|
import async_timeout
|
||||||
import requests
|
import requests
|
||||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -34,10 +39,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Setup a MJPEG IP Camera."""
|
"""Setup a MJPEG IP Camera."""
|
||||||
add_devices([MjpegCamera(config)])
|
hass.loop.create_task(async_add_devices([MjpegCamera(hass, config)]))
|
||||||
|
|
||||||
|
|
||||||
def extract_image_from_mjpeg(stream):
|
def extract_image_from_mjpeg(stream):
|
||||||
@ -52,11 +58,10 @@ def extract_image_from_mjpeg(stream):
|
|||||||
return jpg
|
return jpg
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
class MjpegCamera(Camera):
|
class MjpegCamera(Camera):
|
||||||
"""An implementation of an IP camera that is reachable over a URL."""
|
"""An implementation of an IP camera that is reachable over a URL."""
|
||||||
|
|
||||||
def __init__(self, device_info):
|
def __init__(self, hass, device_info):
|
||||||
"""Initialize a MJPEG camera."""
|
"""Initialize a MJPEG camera."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._name = device_info.get(CONF_NAME)
|
self._name = device_info.get(CONF_NAME)
|
||||||
@ -65,32 +70,61 @@ class MjpegCamera(Camera):
|
|||||||
self._password = device_info.get(CONF_PASSWORD)
|
self._password = device_info.get(CONF_PASSWORD)
|
||||||
self._mjpeg_url = device_info[CONF_MJPEG_URL]
|
self._mjpeg_url = device_info[CONF_MJPEG_URL]
|
||||||
|
|
||||||
def camera_stream(self):
|
self._auth = None
|
||||||
"""Return a MJPEG stream image response directly from the camera."""
|
if self._username and self._password:
|
||||||
|
if self._authentication == HTTP_BASIC_AUTHENTICATION:
|
||||||
|
self._auth = aiohttp.BasicAuth(
|
||||||
|
self._username, password=self._password
|
||||||
|
)
|
||||||
|
|
||||||
|
def camera_image(self):
|
||||||
|
"""Return a still image response from the camera."""
|
||||||
if self._username and self._password:
|
if self._username and self._password:
|
||||||
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||||
auth = HTTPDigestAuth(self._username, self._password)
|
auth = HTTPDigestAuth(self._username, self._password)
|
||||||
else:
|
else:
|
||||||
auth = HTTPBasicAuth(self._username, self._password)
|
auth = HTTPBasicAuth(self._username, self._password)
|
||||||
return requests.get(self._mjpeg_url,
|
req = requests.get(
|
||||||
auth=auth,
|
self._mjpeg_url, auth=auth, stream=True, timeout=10)
|
||||||
stream=True, timeout=10)
|
|
||||||
else:
|
else:
|
||||||
return requests.get(self._mjpeg_url, stream=True, timeout=10)
|
req = requests.get(self._mjpeg_url, stream=True, timeout=10)
|
||||||
|
|
||||||
def camera_image(self):
|
with closing(req) as response:
|
||||||
"""Return a still image response from the camera."""
|
return extract_image_from_mjpeg(response.iter_content(102400))
|
||||||
with closing(self.camera_stream()) as response:
|
|
||||||
return extract_image_from_mjpeg(response.iter_content(1024))
|
|
||||||
|
|
||||||
def mjpeg_stream(self, response):
|
@asyncio.coroutine
|
||||||
|
def handle_async_mjpeg_stream(self, request):
|
||||||
"""Generate an HTTP MJPEG stream from the camera."""
|
"""Generate an HTTP MJPEG stream from the camera."""
|
||||||
stream = self.camera_stream()
|
# aiohttp don't support DigestAuth -> Fallback
|
||||||
return response(
|
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||||
stream.iter_content(chunk_size=1024),
|
yield from super().handle_async_mjpeg_stream(request)
|
||||||
mimetype=stream.headers[CONTENT_TYPE_HEADER],
|
return
|
||||||
direct_passthrough=True
|
|
||||||
|
# connect to stream
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||||
|
stream = yield from self.hass.websession.get(
|
||||||
|
self._mjpeg_url,
|
||||||
|
auth=self._auth
|
||||||
)
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise HTTPGatewayTimeout()
|
||||||
|
|
||||||
|
response = web.StreamResponse()
|
||||||
|
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||||
|
response.enable_chunked_encoding()
|
||||||
|
|
||||||
|
yield from response.prepare(request)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = yield from stream.content.read(102400)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
response.write(data)
|
||||||
|
finally:
|
||||||
|
self.hass.loop.create_task(stream.release())
|
||||||
|
yield from response.write_eof()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -12,7 +12,8 @@ import shutil
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import (CONF_NAME, CONF_FILE_PATH)
|
from homeassistant.const import (CONF_NAME, CONF_FILE_PATH,
|
||||||
|
EVENT_HOMEASSISTANT_STOP)
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -35,7 +36,7 @@ DEFAULT_TIMELAPSE = 1000
|
|||||||
DEFAULT_VERTICAL_FLIP = 0
|
DEFAULT_VERTICAL_FLIP = 0
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_FILE_PATH): cv.isfile,
|
vol.Optional(CONF_FILE_PATH): cv.string,
|
||||||
vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP):
|
vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP):
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
|
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
|
||||||
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_HORIZONTAL_FLIP):
|
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_HORIZONTAL_FLIP):
|
||||||
@ -53,6 +54,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def kill_raspistill(*args):
|
||||||
|
"""Kill any previously running raspistill process.."""
|
||||||
|
subprocess.Popen(['killall', 'raspistill'],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Raspberry Camera."""
|
"""Setup the Raspberry Camera."""
|
||||||
if shutil.which("raspistill") is None:
|
if shutil.which("raspistill") is None:
|
||||||
@ -75,11 +83,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not os.access(setup_config[CONF_FILE_PATH], os.W_OK):
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, kill_raspistill)
|
||||||
_LOGGER.error("File path is not writable")
|
|
||||||
return False
|
try:
|
||||||
|
# Try to create an empty file (or open existing) to ensure we have
|
||||||
|
# proper permissions.
|
||||||
|
open(setup_config[CONF_FILE_PATH], 'a').close()
|
||||||
|
|
||||||
add_devices([RaspberryCamera(setup_config)])
|
add_devices([RaspberryCamera(setup_config)])
|
||||||
|
except PermissionError:
|
||||||
|
_LOGGER.error("File path is not writable")
|
||||||
|
return False
|
||||||
|
except FileNotFoundError:
|
||||||
|
_LOGGER.error("Could not create output file (missing directory?)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class RaspberryCamera(Camera):
|
class RaspberryCamera(Camera):
|
||||||
@ -93,9 +110,7 @@ class RaspberryCamera(Camera):
|
|||||||
self._config = device_info
|
self._config = device_info
|
||||||
|
|
||||||
# Kill if there's raspistill instance
|
# Kill if there's raspistill instance
|
||||||
subprocess.Popen(['killall', 'raspistill'],
|
kill_raspistill()
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.STDOUT)
|
|
||||||
|
|
||||||
cmd_args = [
|
cmd_args = [
|
||||||
'raspistill', '--nopreview', '-o', device_info[CONF_FILE_PATH],
|
'raspistill', '--nopreview', '-o', device_info[CONF_FILE_PATH],
|
||||||
|
@ -4,28 +4,30 @@ Support for Synology Surveillance Station Cameras.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/camera.synology/
|
https://home-assistant.io/components/camera.synology/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import requests
|
from aiohttp import web
|
||||||
|
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
||||||
|
import async_timeout
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||||
CONF_URL, CONF_WHITELIST)
|
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL)
|
||||||
from homeassistant.components.camera import (
|
from homeassistant.components.camera import (
|
||||||
Camera, PLATFORM_SCHEMA)
|
Camera, PLATFORM_SCHEMA)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.util.async import run_coroutine_threadsafe
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
# pylint: disable=too-many-locals
|
|
||||||
DEFAULT_NAME = 'Synology Camera'
|
DEFAULT_NAME = 'Synology Camera'
|
||||||
DEFAULT_STREAM_ID = '0'
|
DEFAULT_STREAM_ID = '0'
|
||||||
TIMEOUT = 5
|
TIMEOUT = 5
|
||||||
CONF_CAMERA_NAME = 'camera_name'
|
CONF_CAMERA_NAME = 'camera_name'
|
||||||
CONF_STREAM_ID = 'stream_id'
|
CONF_STREAM_ID = 'stream_id'
|
||||||
CONF_VALID_CERT = 'valid_cert'
|
|
||||||
|
|
||||||
QUERY_CGI = 'query.cgi'
|
QUERY_CGI = 'query.cgi'
|
||||||
QUERY_API = 'SYNO.API.Info'
|
QUERY_API = 'SYNO.API.Info'
|
||||||
@ -38,6 +40,7 @@ WEBAPI_PATH = '/webapi/'
|
|||||||
AUTH_PATH = 'auth.cgi'
|
AUTH_PATH = 'auth.cgi'
|
||||||
CAMERA_PATH = 'camera.cgi'
|
CAMERA_PATH = 'camera.cgi'
|
||||||
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
|
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
|
||||||
|
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||||
|
|
||||||
SYNO_API_URL = '{0}{1}{2}'
|
SYNO_API_URL = '{0}{1}{2}'
|
||||||
|
|
||||||
@ -47,89 +50,136 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Required(CONF_URL): cv.string,
|
vol.Required(CONF_URL): cv.string,
|
||||||
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
|
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
|
||||||
vol.Optional(CONF_VALID_CERT, default=True): cv.boolean,
|
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
@asyncio.coroutine
|
||||||
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Setup a Synology IP Camera."""
|
"""Setup a Synology IP Camera."""
|
||||||
# Determine API to use for authentication
|
# Determine API to use for authentication
|
||||||
syno_api_url = SYNO_API_URL.format(config.get(CONF_URL),
|
syno_api_url = SYNO_API_URL.format(
|
||||||
WEBAPI_PATH,
|
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
|
||||||
QUERY_CGI)
|
|
||||||
query_payload = {'api': QUERY_API,
|
query_payload = {
|
||||||
|
'api': QUERY_API,
|
||||||
'method': 'Query',
|
'method': 'Query',
|
||||||
'version': '1',
|
'version': '1',
|
||||||
'query': 'SYNO.'}
|
'query': 'SYNO.'
|
||||||
query_req = requests.get(syno_api_url,
|
}
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||||
|
query_req = yield from hass.websession.get(
|
||||||
|
syno_api_url,
|
||||||
params=query_payload,
|
params=query_payload,
|
||||||
verify=config.get(CONF_VALID_CERT),
|
verify_ssl=config.get(CONF_VERIFY_SSL)
|
||||||
timeout=TIMEOUT)
|
)
|
||||||
query_resp = query_req.json()
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.error("Timeout on %s", syno_api_url)
|
||||||
|
return False
|
||||||
|
|
||||||
|
query_resp = yield from query_req.json()
|
||||||
auth_path = query_resp['data'][AUTH_API]['path']
|
auth_path = query_resp['data'][AUTH_API]['path']
|
||||||
camera_api = query_resp['data'][CAMERA_API]['path']
|
camera_api = query_resp['data'][CAMERA_API]['path']
|
||||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||||
|
|
||||||
|
# cleanup
|
||||||
|
yield from query_req.release()
|
||||||
|
|
||||||
# Authticate to NAS to get a session id
|
# Authticate to NAS to get a session id
|
||||||
syno_auth_url = SYNO_API_URL.format(config.get(CONF_URL),
|
syno_auth_url = SYNO_API_URL.format(
|
||||||
WEBAPI_PATH,
|
config.get(CONF_URL), WEBAPI_PATH, auth_path)
|
||||||
auth_path)
|
|
||||||
session_id = get_session_id(config.get(CONF_USERNAME),
|
session_id = yield from get_session_id(
|
||||||
|
hass,
|
||||||
|
config.get(CONF_USERNAME),
|
||||||
config.get(CONF_PASSWORD),
|
config.get(CONF_PASSWORD),
|
||||||
syno_auth_url,
|
syno_auth_url,
|
||||||
config.get(CONF_VALID_CERT))
|
config.get(CONF_VERIFY_SSL)
|
||||||
|
)
|
||||||
|
|
||||||
# Use SessionID to get cameras in system
|
# Use SessionID to get cameras in system
|
||||||
syno_camera_url = SYNO_API_URL.format(config.get(CONF_URL),
|
syno_camera_url = SYNO_API_URL.format(
|
||||||
WEBAPI_PATH,
|
config.get(CONF_URL), WEBAPI_PATH, camera_api)
|
||||||
camera_api)
|
|
||||||
camera_payload = {'api': CAMERA_API,
|
camera_payload = {
|
||||||
|
'api': CAMERA_API,
|
||||||
'method': 'List',
|
'method': 'List',
|
||||||
'version': '1'}
|
'version': '1'
|
||||||
camera_req = requests.get(syno_camera_url,
|
}
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||||
|
camera_req = yield from hass.websession.get(
|
||||||
|
syno_camera_url,
|
||||||
params=camera_payload,
|
params=camera_payload,
|
||||||
verify=config.get(CONF_VALID_CERT),
|
verify_ssl=config.get(CONF_VERIFY_SSL),
|
||||||
timeout=TIMEOUT,
|
cookies={'id': session_id}
|
||||||
cookies={'id': session_id})
|
)
|
||||||
camera_resp = camera_req.json()
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.error("Timeout on %s", syno_camera_url)
|
||||||
|
return False
|
||||||
|
|
||||||
|
camera_resp = yield from camera_req.json()
|
||||||
cameras = camera_resp['data']['cameras']
|
cameras = camera_resp['data']['cameras']
|
||||||
|
yield from camera_req.release()
|
||||||
|
|
||||||
|
# add cameras
|
||||||
|
devices = []
|
||||||
|
tasks = []
|
||||||
for camera in cameras:
|
for camera in cameras:
|
||||||
if not config.get(CONF_WHITELIST):
|
if not config.get(CONF_WHITELIST):
|
||||||
camera_id = camera['id']
|
camera_id = camera['id']
|
||||||
snapshot_path = camera['snapshot_path']
|
snapshot_path = camera['snapshot_path']
|
||||||
|
|
||||||
add_devices([SynologyCamera(config,
|
device = SynologyCamera(
|
||||||
|
config,
|
||||||
camera_id,
|
camera_id,
|
||||||
camera['name'],
|
camera['name'],
|
||||||
snapshot_path,
|
snapshot_path,
|
||||||
streaming_path,
|
streaming_path,
|
||||||
camera_path,
|
camera_path,
|
||||||
auth_path)])
|
auth_path
|
||||||
|
)
|
||||||
|
tasks.append(device.async_read_sid())
|
||||||
|
devices.append(device)
|
||||||
|
|
||||||
|
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||||
|
hass.loop.create_task(async_add_devices(devices))
|
||||||
|
|
||||||
|
|
||||||
def get_session_id(username, password, login_url, valid_cert):
|
@asyncio.coroutine
|
||||||
|
def get_session_id(hass, username, password, login_url, valid_cert):
|
||||||
"""Get a session id."""
|
"""Get a session id."""
|
||||||
auth_payload = {'api': AUTH_API,
|
auth_payload = {
|
||||||
|
'api': AUTH_API,
|
||||||
'method': 'Login',
|
'method': 'Login',
|
||||||
'version': '2',
|
'version': '2',
|
||||||
'account': username,
|
'account': username,
|
||||||
'passwd': password,
|
'passwd': password,
|
||||||
'session': 'SurveillanceStation',
|
'session': 'SurveillanceStation',
|
||||||
'format': 'sid'}
|
'format': 'sid'
|
||||||
auth_req = requests.get(login_url,
|
}
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||||
|
auth_req = yield from hass.websession.get(
|
||||||
|
login_url,
|
||||||
params=auth_payload,
|
params=auth_payload,
|
||||||
verify=valid_cert,
|
verify_ssl=valid_cert
|
||||||
timeout=TIMEOUT)
|
)
|
||||||
auth_resp = auth_req.json()
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.error("Timeout on %s", login_url)
|
||||||
|
return False
|
||||||
|
|
||||||
|
auth_resp = yield from auth_req.json()
|
||||||
|
yield from auth_req.release()
|
||||||
|
|
||||||
return auth_resp['data']['sid']
|
return auth_resp['data']['sid']
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
class SynologyCamera(Camera):
|
class SynologyCamera(Camera):
|
||||||
"""An implementation of a Synology NAS based IP camera."""
|
"""An implementation of a Synology NAS based IP camera."""
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def __init__(self, config, camera_id, camera_name,
|
def __init__(self, config, camera_id, camera_name,
|
||||||
snapshot_path, streaming_path, camera_path, auth_path):
|
snapshot_path, streaming_path, camera_path, auth_path):
|
||||||
"""Initialize a Synology Surveillance Station camera."""
|
"""Initialize a Synology Surveillance Station camera."""
|
||||||
@ -142,80 +192,98 @@ class SynologyCamera(Camera):
|
|||||||
self._login_url = config.get(CONF_URL) + '/webapi/' + 'auth.cgi'
|
self._login_url = config.get(CONF_URL) + '/webapi/' + 'auth.cgi'
|
||||||
self._camera_name = config.get(CONF_CAMERA_NAME)
|
self._camera_name = config.get(CONF_CAMERA_NAME)
|
||||||
self._stream_id = config.get(CONF_STREAM_ID)
|
self._stream_id = config.get(CONF_STREAM_ID)
|
||||||
self._valid_cert = config.get(CONF_VALID_CERT)
|
self._valid_cert = config.get(CONF_VERIFY_SSL)
|
||||||
self._camera_id = camera_id
|
self._camera_id = camera_id
|
||||||
self._snapshot_path = snapshot_path
|
self._snapshot_path = snapshot_path
|
||||||
self._streaming_path = streaming_path
|
self._streaming_path = streaming_path
|
||||||
self._camera_path = camera_path
|
self._camera_path = camera_path
|
||||||
self._auth_path = auth_path
|
self._auth_path = auth_path
|
||||||
|
self._session_id = None
|
||||||
|
|
||||||
self._session_id = get_session_id(self._username,
|
@asyncio.coroutine
|
||||||
|
def async_read_sid(self):
|
||||||
|
"""Get a session id."""
|
||||||
|
self._session_id = yield from get_session_id(
|
||||||
|
self.hass,
|
||||||
|
self._username,
|
||||||
self._password,
|
self._password,
|
||||||
self._login_url,
|
self._login_url,
|
||||||
self._valid_cert)
|
self._valid_cert
|
||||||
|
)
|
||||||
def get_sid(self):
|
|
||||||
"""Get a session id."""
|
|
||||||
auth_payload = {'api': AUTH_API,
|
|
||||||
'method': 'Login',
|
|
||||||
'version': '2',
|
|
||||||
'account': self._username,
|
|
||||||
'passwd': self._password,
|
|
||||||
'session': 'SurveillanceStation',
|
|
||||||
'format': 'sid'}
|
|
||||||
auth_req = requests.get(self._login_url,
|
|
||||||
params=auth_payload,
|
|
||||||
verify=self._valid_cert,
|
|
||||||
timeout=TIMEOUT)
|
|
||||||
auth_resp = auth_req.json()
|
|
||||||
self._session_id = auth_resp['data']['sid']
|
|
||||||
|
|
||||||
def camera_image(self):
|
def camera_image(self):
|
||||||
|
"""Return bytes of camera image."""
|
||||||
|
return run_coroutine_threadsafe(
|
||||||
|
self.async_camera_image(), self.hass.loop).result()
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_camera_image(self):
|
||||||
"""Return a still image response from the camera."""
|
"""Return a still image response from the camera."""
|
||||||
image_url = SYNO_API_URL.format(self._synology_url,
|
image_url = SYNO_API_URL.format(
|
||||||
WEBAPI_PATH,
|
self._synology_url, WEBAPI_PATH, self._camera_path)
|
||||||
self._camera_path)
|
|
||||||
image_payload = {'api': CAMERA_API,
|
image_payload = {
|
||||||
|
'api': CAMERA_API,
|
||||||
'method': 'GetSnapshot',
|
'method': 'GetSnapshot',
|
||||||
'version': '1',
|
'version': '1',
|
||||||
'cameraId': self._camera_id}
|
'cameraId': self._camera_id
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
response = requests.get(image_url,
|
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||||
|
response = yield from self.hass.websession.get(
|
||||||
|
image_url,
|
||||||
params=image_payload,
|
params=image_payload,
|
||||||
timeout=TIMEOUT,
|
verify_ssl=self._valid_cert,
|
||||||
verify=self._valid_cert,
|
cookies={'id': self._session_id}
|
||||||
cookies={'id': self._session_id})
|
)
|
||||||
except requests.exceptions.RequestException as error:
|
except asyncio.TimeoutError:
|
||||||
_LOGGER.error('Error getting camera image: %s', error)
|
_LOGGER.error("Timeout on %s", image_url)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return response.content
|
image = yield from response.read()
|
||||||
|
yield from response.release()
|
||||||
|
|
||||||
def camera_stream(self):
|
return image
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def handle_async_mjpeg_stream(self, request):
|
||||||
"""Return a MJPEG stream image response directly from the camera."""
|
"""Return a MJPEG stream image response directly from the camera."""
|
||||||
streaming_url = SYNO_API_URL.format(self._synology_url,
|
streaming_url = SYNO_API_URL.format(
|
||||||
WEBAPI_PATH,
|
self._synology_url, WEBAPI_PATH, self._streaming_path)
|
||||||
self._streaming_path)
|
|
||||||
streaming_payload = {'api': STREAMING_API,
|
streaming_payload = {
|
||||||
|
'api': STREAMING_API,
|
||||||
'method': 'Stream',
|
'method': 'Stream',
|
||||||
'version': '1',
|
'version': '1',
|
||||||
'cameraId': self._camera_id,
|
'cameraId': self._camera_id,
|
||||||
'format': 'mjpeg'}
|
'format': 'mjpeg'
|
||||||
response = requests.get(streaming_url,
|
}
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||||
|
stream = yield from self.hass.websession.get(
|
||||||
|
streaming_url,
|
||||||
payload=streaming_payload,
|
payload=streaming_payload,
|
||||||
stream=True,
|
verify_ssl=self._valid_cert,
|
||||||
timeout=TIMEOUT,
|
cookies={'id': self._session_id}
|
||||||
cookies={'id': self._session_id})
|
|
||||||
return response
|
|
||||||
|
|
||||||
def mjpeg_steam(self, response):
|
|
||||||
"""Generate an HTTP MJPEG Stream from the Synology NAS."""
|
|
||||||
stream = self.camera_stream()
|
|
||||||
return response(
|
|
||||||
stream.iter_content(chunk_size=1024),
|
|
||||||
mimetype=stream.headers['CONTENT_TYPE_HEADER'],
|
|
||||||
direct_passthrough=True
|
|
||||||
)
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise HTTPGatewayTimeout()
|
||||||
|
|
||||||
|
response = web.StreamResponse()
|
||||||
|
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||||
|
response.enable_chunked_encoding()
|
||||||
|
|
||||||
|
yield from response.prepare(request)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = yield from stream.content.read(102400)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
response.write(data)
|
||||||
|
finally:
|
||||||
|
self.hass.loop.create_task(stream.release())
|
||||||
|
yield from response.write_eof()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -123,7 +123,6 @@ def set_aux_heat(hass, aux_heat, entity_id=None):
|
|||||||
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
|
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def set_temperature(hass, temperature=None, entity_id=None,
|
def set_temperature(hass, temperature=None, entity_id=None,
|
||||||
target_temp_high=None, target_temp_low=None,
|
target_temp_high=None, target_temp_low=None,
|
||||||
operation_mode=None):
|
operation_mode=None):
|
||||||
@ -181,7 +180,6 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
|
|||||||
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
|
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup climate devices."""
|
"""Setup climate devices."""
|
||||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||||
@ -364,7 +362,7 @@ def setup(hass, config):
|
|||||||
class ClimateDevice(Entity):
|
class ClimateDevice(Entity):
|
||||||
"""Representation of a climate device."""
|
"""Representation of a climate device."""
|
||||||
|
|
||||||
# pylint: disable=too-many-public-methods,no-self-use
|
# pylint: disable=no-self-use
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the current state."""
|
"""Return the current state."""
|
||||||
|
@ -21,11 +21,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-public-methods
|
|
||||||
class DemoClimate(ClimateDevice):
|
class DemoClimate(ClimateDevice):
|
||||||
"""Representation of a demo climate device."""
|
"""Representation of a demo climate device."""
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
def __init__(self, name, target_temperature, unit_of_measurement,
|
def __init__(self, name, target_temperature, unit_of_measurement,
|
||||||
away, current_temperature, current_fan_mode,
|
away, current_temperature, current_fan_mode,
|
||||||
target_humidity, current_humidity, current_swing_mode,
|
target_humidity, current_humidity, current_swing_mode,
|
||||||
|
@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
|
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-public-methods, abstract-method
|
|
||||||
class Thermostat(ClimateDevice):
|
class Thermostat(ClimateDevice):
|
||||||
"""A thermostat class for Ecobee."""
|
"""A thermostat class for Ecobee."""
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
add_devices(devices)
|
add_devices(devices)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes, import-error, abstract-method
|
# pylint: disable=import-error
|
||||||
class EQ3BTSmartThermostat(ClimateDevice):
|
class EQ3BTSmartThermostat(ClimateDevice):
|
||||||
"""Representation of a eQ-3 Bluetooth Smart thermostat."""
|
"""Representation of a eQ-3 Bluetooth Smart thermostat."""
|
||||||
|
|
||||||
|
@ -62,11 +62,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
target_temp, ac_mode, min_cycle_duration)])
|
target_temp, ac_mode, min_cycle_duration)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
|
||||||
class GenericThermostat(ClimateDevice):
|
class GenericThermostat(ClimateDevice):
|
||||||
"""Representation of a GenericThermostat device."""
|
"""Representation of a GenericThermostat device."""
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration):
|
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration):
|
||||||
"""Initialize the thermostat."""
|
"""Initialize the thermostat."""
|
||||||
|
@ -56,7 +56,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
class HeatmiserV3Thermostat(ClimateDevice):
|
class HeatmiserV3Thermostat(ClimateDevice):
|
||||||
"""Representation of a HeatmiserV3 thermostat."""
|
"""Representation of a HeatmiserV3 thermostat."""
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
|
||||||
def __init__(self, heatmiser, device, name, serport):
|
def __init__(self, heatmiser, device, name, serport):
|
||||||
"""Initialize the thermostat."""
|
"""Initialize the thermostat."""
|
||||||
self.heatmiser = heatmiser
|
self.heatmiser = heatmiser
|
||||||
|
@ -36,7 +36,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class HMThermostat(homematic.HMDevice, ClimateDevice):
|
class HMThermostat(homematic.HMDevice, ClimateDevice):
|
||||||
"""Representation of a Homematic thermostat."""
|
"""Representation of a Homematic thermostat."""
|
||||||
|
|
||||||
|
@ -100,7 +100,6 @@ def _setup_us(username, password, config, add_devices):
|
|||||||
class RoundThermostat(ClimateDevice):
|
class RoundThermostat(ClimateDevice):
|
||||||
"""Representation of a Honeywell Round Connected thermostat."""
|
"""Representation of a Honeywell Round Connected thermostat."""
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
|
||||||
def __init__(self, device, zone_id, master, away_temp):
|
def __init__(self, device, zone_id, master, away_temp):
|
||||||
"""Initialize the thermostat."""
|
"""Initialize the thermostat."""
|
||||||
self.device = device
|
self.device = device
|
||||||
@ -197,7 +196,6 @@ class RoundThermostat(ClimateDevice):
|
|||||||
self._is_dhw = False
|
self._is_dhw = False
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class HoneywellUSThermostat(ClimateDevice):
|
class HoneywellUSThermostat(ClimateDevice):
|
||||||
"""Representation of a Honeywell US Thermostat."""
|
"""Representation of a Honeywell US Thermostat."""
|
||||||
|
|
||||||
|
@ -37,7 +37,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
map_sv_types, devices, add_devices, MySensorsHVAC))
|
map_sv_types, devices, add_devices, MySensorsHVAC))
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-public-methods
|
|
||||||
class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
||||||
"""Representation of a MySensorsHVAC hvac."""
|
"""Representation of a MySensorsHVAC hvac."""
|
||||||
|
|
||||||
|
@ -5,8 +5,10 @@ For more details about this platform, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/climate.nest/
|
https://home-assistant.io/components/climate.nest/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import homeassistant.components.nest as nest
|
|
||||||
|
from homeassistant.components.nest import DATA_NEST
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
|
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
|
||||||
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||||
@ -26,11 +28,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Nest thermostat."""
|
"""Setup the Nest thermostat."""
|
||||||
temp_unit = hass.config.units.temperature_unit
|
temp_unit = hass.config.units.temperature_unit
|
||||||
add_devices([NestThermostat(structure, device, temp_unit)
|
add_devices(
|
||||||
for structure, device in nest.devices()])
|
[NestThermostat(structure, device, temp_unit)
|
||||||
|
for structure, device in hass.data[DATA_NEST].devices()],
|
||||||
|
True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method,too-many-public-methods
|
|
||||||
class NestThermostat(ClimateDevice):
|
class NestThermostat(ClimateDevice):
|
||||||
"""Representation of a Nest thermostat."""
|
"""Representation of a Nest thermostat."""
|
||||||
|
|
||||||
@ -54,18 +58,33 @@ class NestThermostat(ClimateDevice):
|
|||||||
if self.device.can_heat and self.device.can_cool:
|
if self.device.can_heat and self.device.can_cool:
|
||||||
self._operation_list.append(STATE_AUTO)
|
self._operation_list.append(STATE_AUTO)
|
||||||
|
|
||||||
|
# feature of device
|
||||||
|
self._has_humidifier = self.device.has_humidifier
|
||||||
|
self._has_dehumidifier = self.device.has_dehumidifier
|
||||||
|
self._has_fan = self.device.has_fan
|
||||||
|
|
||||||
|
# data attributes
|
||||||
|
self._away = None
|
||||||
|
self._location = None
|
||||||
|
self._name = None
|
||||||
|
self._humidity = None
|
||||||
|
self._target_humidity = None
|
||||||
|
self._target_temperature = None
|
||||||
|
self._temperature = None
|
||||||
|
self._mode = None
|
||||||
|
self._fan = None
|
||||||
|
self._away_temperature = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the nest, if any."""
|
"""Return the name of the nest, if any."""
|
||||||
location = self.device.where
|
if self._location is None:
|
||||||
name = self.device.name
|
return self._name
|
||||||
if location is None:
|
|
||||||
return name
|
|
||||||
else:
|
else:
|
||||||
if name == '':
|
if self._name == '':
|
||||||
return location.capitalize()
|
return self._location.capitalize()
|
||||||
else:
|
else:
|
||||||
return location.capitalize() + '(' + name + ')'
|
return self._location.capitalize() + '(' + self._name + ')'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temperature_unit(self):
|
def temperature_unit(self):
|
||||||
@ -75,11 +94,11 @@ class NestThermostat(ClimateDevice):
|
|||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the device specific state attributes."""
|
"""Return the device specific state attributes."""
|
||||||
if self.device.has_humidifier or self.device.has_dehumidifier:
|
if self._has_humidifier or self._has_dehumidifier:
|
||||||
# Move these to Thermostat Device and make them global
|
# Move these to Thermostat Device and make them global
|
||||||
return {
|
return {
|
||||||
"humidity": self.device.humidity,
|
"humidity": self._humidity,
|
||||||
"target_humidity": self.device.target_humidity,
|
"target_humidity": self._target_humidity,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# No way to control humidity not show setting
|
# No way to control humidity not show setting
|
||||||
@ -88,18 +107,18 @@ class NestThermostat(ClimateDevice):
|
|||||||
@property
|
@property
|
||||||
def current_temperature(self):
|
def current_temperature(self):
|
||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
return self.device.temperature
|
return self._temperature
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_operation(self):
|
def current_operation(self):
|
||||||
"""Return current operation ie. heat, cool, idle."""
|
"""Return current operation ie. heat, cool, idle."""
|
||||||
if self.device.mode == 'cool':
|
if self._mode == 'cool':
|
||||||
return STATE_COOL
|
return STATE_COOL
|
||||||
elif self.device.mode == 'heat':
|
elif self._mode == 'heat':
|
||||||
return STATE_HEAT
|
return STATE_HEAT
|
||||||
elif self.device.mode == 'range':
|
elif self._mode == 'range':
|
||||||
return STATE_AUTO
|
return STATE_AUTO
|
||||||
elif self.device.mode == 'off':
|
elif self._mode == 'off':
|
||||||
return STATE_OFF
|
return STATE_OFF
|
||||||
else:
|
else:
|
||||||
return STATE_UNKNOWN
|
return STATE_UNKNOWN
|
||||||
@ -107,37 +126,37 @@ class NestThermostat(ClimateDevice):
|
|||||||
@property
|
@property
|
||||||
def target_temperature(self):
|
def target_temperature(self):
|
||||||
"""Return the temperature we try to reach."""
|
"""Return the temperature we try to reach."""
|
||||||
if self.device.mode != 'range' and not self.is_away_mode_on:
|
if self._mode != 'range' and not self.is_away_mode_on:
|
||||||
return self.device.target
|
return self._target_temperature
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature_low(self):
|
def target_temperature_low(self):
|
||||||
"""Return the lower bound temperature we try to reach."""
|
"""Return the lower bound temperature we try to reach."""
|
||||||
if self.is_away_mode_on and self.device.away_temperature[0]:
|
if self.is_away_mode_on and self._away_temperature[0]:
|
||||||
# away_temperature is always a low, high tuple
|
# away_temperature is always a low, high tuple
|
||||||
return self.device.away_temperature[0]
|
return self._away_temperature[0]
|
||||||
if self.device.mode == 'range':
|
if self._mode == 'range':
|
||||||
return self.device.target[0]
|
return self._target_temperature[0]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature_high(self):
|
def target_temperature_high(self):
|
||||||
"""Return the upper bound temperature we try to reach."""
|
"""Return the upper bound temperature we try to reach."""
|
||||||
if self.is_away_mode_on and self.device.away_temperature[1]:
|
if self.is_away_mode_on and self._away_temperature[1]:
|
||||||
# away_temperature is always a low, high tuple
|
# away_temperature is always a low, high tuple
|
||||||
return self.device.away_temperature[1]
|
return self._away_temperature[1]
|
||||||
if self.device.mode == 'range':
|
if self._mode == 'range':
|
||||||
return self.device.target[1]
|
return self._target_temperature[1]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_away_mode_on(self):
|
def is_away_mode_on(self):
|
||||||
"""Return if away mode is on."""
|
"""Return if away mode is on."""
|
||||||
return self.structure.away
|
return self._away
|
||||||
|
|
||||||
def set_temperature(self, **kwargs):
|
def set_temperature(self, **kwargs):
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
@ -145,7 +164,7 @@ class NestThermostat(ClimateDevice):
|
|||||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||||
if target_temp_low is not None and target_temp_high is not None:
|
if target_temp_low is not None and target_temp_high is not None:
|
||||||
|
|
||||||
if self.device.mode == 'range':
|
if self._mode == 'range':
|
||||||
temp = (target_temp_low, target_temp_high)
|
temp = (target_temp_low, target_temp_high)
|
||||||
else:
|
else:
|
||||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||||
@ -179,9 +198,9 @@ class NestThermostat(ClimateDevice):
|
|||||||
@property
|
@property
|
||||||
def current_fan_mode(self):
|
def current_fan_mode(self):
|
||||||
"""Return whether the fan is on."""
|
"""Return whether the fan is on."""
|
||||||
if self.device.has_fan:
|
if self._has_fan:
|
||||||
# Return whether the fan is on
|
# Return whether the fan is on
|
||||||
return STATE_ON if self.device.fan else STATE_AUTO
|
return STATE_ON if self._fan else STATE_AUTO
|
||||||
else:
|
else:
|
||||||
# No Fan available so disable slider
|
# No Fan available so disable slider
|
||||||
return None
|
return None
|
||||||
@ -198,7 +217,7 @@ class NestThermostat(ClimateDevice):
|
|||||||
@property
|
@property
|
||||||
def min_temp(self):
|
def min_temp(self):
|
||||||
"""Identify min_temp in Nest API or defaults if not available."""
|
"""Identify min_temp in Nest API or defaults if not available."""
|
||||||
temp = self.device.away_temperature.low
|
temp = self._away_temperature[0]
|
||||||
if temp is None:
|
if temp is None:
|
||||||
return super().min_temp
|
return super().min_temp
|
||||||
else:
|
else:
|
||||||
@ -207,12 +226,21 @@ class NestThermostat(ClimateDevice):
|
|||||||
@property
|
@property
|
||||||
def max_temp(self):
|
def max_temp(self):
|
||||||
"""Identify max_temp in Nest API or defaults if not available."""
|
"""Identify max_temp in Nest API or defaults if not available."""
|
||||||
temp = self.device.away_temperature.high
|
temp = self._away_temperature[1]
|
||||||
if temp is None:
|
if temp is None:
|
||||||
return super().max_temp
|
return super().max_temp
|
||||||
else:
|
else:
|
||||||
return temp
|
return temp
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Python-nest has its own mechanism for staying up to date."""
|
"""Cache value from Python-nest."""
|
||||||
pass
|
self._location = self.device.where
|
||||||
|
self._name = self.device.name
|
||||||
|
self._humidity = self.device.humidity,
|
||||||
|
self._target_humidity = self.device.target_humidity,
|
||||||
|
self._temperature = self.device.temperature
|
||||||
|
self._mode = self.device.mode
|
||||||
|
self._target_temperature = self.device.target
|
||||||
|
self._fan = self.device.fan
|
||||||
|
self._away = self.structure.away
|
||||||
|
self._away_temperature = self.device.away_temperature
|
||||||
|
@ -54,7 +54,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class NetatmoThermostat(ClimateDevice):
|
class NetatmoThermostat(ClimateDevice):
|
||||||
"""Representation a Netatmo thermostat."""
|
"""Representation a Netatmo thermostat."""
|
||||||
|
|
||||||
|
@ -36,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
add_devices([ProliphixThermostat(pdp)])
|
add_devices([ProliphixThermostat(pdp)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class ProliphixThermostat(ClimateDevice):
|
class ProliphixThermostat(ClimateDevice):
|
||||||
"""Representation a Proliphix thermostat."""
|
"""Representation a Proliphix thermostat."""
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ https://home-assistant.io/components/climate.radiotherm/
|
|||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from urllib.error import URLError
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -52,14 +51,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
try:
|
try:
|
||||||
tstat = radiotherm.get_thermostat(host)
|
tstat = radiotherm.get_thermostat(host)
|
||||||
tstats.append(RadioThermostat(tstat, hold_temp))
|
tstats.append(RadioThermostat(tstat, hold_temp))
|
||||||
except (URLError, OSError):
|
except OSError:
|
||||||
_LOGGER.exception("Unable to connect to Radio Thermostat: %s",
|
_LOGGER.exception("Unable to connect to Radio Thermostat: %s",
|
||||||
host)
|
host)
|
||||||
|
|
||||||
add_devices(tstats)
|
add_devices(tstats)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class RadioThermostat(ClimateDevice):
|
class RadioThermostat(ClimateDevice):
|
||||||
"""Representation of a Radio Thermostat."""
|
"""Representation of a Radio Thermostat."""
|
||||||
|
|
||||||
|
@ -8,7 +8,10 @@ import logging
|
|||||||
|
|
||||||
from homeassistant.util import convert
|
from homeassistant.util import convert
|
||||||
from homeassistant.components.climate import ClimateDevice
|
from homeassistant.components.climate import ClimateDevice
|
||||||
from homeassistant.const import TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
from homeassistant.const import (
|
||||||
|
TEMP_FAHRENHEIT,
|
||||||
|
TEMP_CELSIUS,
|
||||||
|
ATTR_TEMPERATURE)
|
||||||
|
|
||||||
from homeassistant.components.vera import (
|
from homeassistant.components.vera import (
|
||||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||||
@ -28,7 +31,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
device in VERA_DEVICES['climate'])
|
device in VERA_DEVICES['climate'])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class VeraThermostat(VeraDevice, ClimateDevice):
|
class VeraThermostat(VeraDevice, ClimateDevice):
|
||||||
"""Representation of a Vera Thermostat."""
|
"""Representation of a Vera Thermostat."""
|
||||||
|
|
||||||
@ -95,8 +97,14 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
|||||||
@property
|
@property
|
||||||
def temperature_unit(self):
|
def temperature_unit(self):
|
||||||
"""Return the unit of measurement."""
|
"""Return the unit of measurement."""
|
||||||
|
vera_temp_units = (
|
||||||
|
self.vera_device.vera_controller.temperature_units)
|
||||||
|
|
||||||
|
if vera_temp_units == 'F':
|
||||||
return TEMP_FAHRENHEIT
|
return TEMP_FAHRENHEIT
|
||||||
|
|
||||||
|
return TEMP_CELSIUS
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self):
|
def current_temperature(self):
|
||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
|
@ -69,11 +69,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
discovery_info, zwave.NETWORK)
|
discovery_info, zwave.NETWORK)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||||
"""Represents a ZWave Climate device."""
|
"""Represents a ZWave Climate device."""
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
def __init__(self, value, temp_unit):
|
def __init__(self, value, temp_unit):
|
||||||
"""Initialize the zwave climate device."""
|
"""Initialize the zwave climate device."""
|
||||||
from openzwave.network import ZWaveNetwork
|
from openzwave.network import ZWaveNetwork
|
||||||
@ -84,6 +82,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
self._current_temperature = None
|
self._current_temperature = None
|
||||||
self._current_operation = None
|
self._current_operation = None
|
||||||
self._operation_list = None
|
self._operation_list = None
|
||||||
|
self._operating_state = None
|
||||||
self._current_fan_mode = None
|
self._current_fan_mode = None
|
||||||
self._fan_list = None
|
self._fan_list = None
|
||||||
self._current_swing_mode = None
|
self._current_swing_mode = None
|
||||||
@ -182,6 +181,11 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
_LOGGER.debug("Device can't set setpoint based on operation mode."
|
_LOGGER.debug("Device can't set setpoint based on operation mode."
|
||||||
" Defaulting to index=1")
|
" Defaulting to index=1")
|
||||||
self._target_temperature = int(value.data)
|
self._target_temperature = int(value.data)
|
||||||
|
# Operating state
|
||||||
|
for value in (self._node.get_values(
|
||||||
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE)
|
||||||
|
.values()):
|
||||||
|
self._operating_state = value.data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
@ -323,3 +327,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
value.index == 33:
|
value.index == 33:
|
||||||
value.data = bytes(swing_mode, 'utf-8')
|
value.data = bytes(swing_mode, 'utf-8')
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the device specific state attributes."""
|
||||||
|
if self._operating_state:
|
||||||
|
return {
|
||||||
|
"operating_state": self._operating_state,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
@ -8,7 +8,8 @@ the user has submitted configuration information.
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME
|
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
|
||||||
|
ATTR_ENTITY_PICTURE
|
||||||
from homeassistant.helpers.entity import generate_entity_id
|
from homeassistant.helpers.entity import generate_entity_id
|
||||||
|
|
||||||
_INSTANCES = {}
|
_INSTANCES = {}
|
||||||
@ -33,10 +34,10 @@ STATE_CONFIGURE = 'configure'
|
|||||||
STATE_CONFIGURED = 'configured'
|
STATE_CONFIGURED = 'configured'
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def request_config(
|
def request_config(
|
||||||
hass, name, callback, description=None, description_image=None,
|
hass, name, callback, description=None, description_image=None,
|
||||||
submit_caption=None, fields=None, link_name=None, link_url=None):
|
submit_caption=None, fields=None, link_name=None, link_url=None,
|
||||||
|
entity_picture=None):
|
||||||
"""Create a new request for configuration.
|
"""Create a new request for configuration.
|
||||||
|
|
||||||
Will return an ID to be used for sequent calls.
|
Will return an ID to be used for sequent calls.
|
||||||
@ -46,7 +47,7 @@ def request_config(
|
|||||||
request_id = instance.request_config(
|
request_id = instance.request_config(
|
||||||
name, callback,
|
name, callback,
|
||||||
description, description_image, submit_caption,
|
description, description_image, submit_caption,
|
||||||
fields, link_name, link_url)
|
fields, link_name, link_url, entity_picture)
|
||||||
|
|
||||||
_REQUESTS[request_id] = instance
|
_REQUESTS[request_id] = instance
|
||||||
|
|
||||||
@ -100,11 +101,10 @@ class Configurator(object):
|
|||||||
hass.services.register(
|
hass.services.register(
|
||||||
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
|
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def request_config(
|
def request_config(
|
||||||
self, name, callback,
|
self, name, callback,
|
||||||
description, description_image, submit_caption,
|
description, description_image, submit_caption,
|
||||||
fields, link_name, link_url):
|
fields, link_name, link_url, entity_picture):
|
||||||
"""Setup a request for configuration."""
|
"""Setup a request for configuration."""
|
||||||
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
|
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
|
||||||
|
|
||||||
@ -119,6 +119,7 @@ class Configurator(object):
|
|||||||
ATTR_CONFIGURE_ID: request_id,
|
ATTR_CONFIGURE_ID: request_id,
|
||||||
ATTR_FIELDS: fields,
|
ATTR_FIELDS: fields,
|
||||||
ATTR_FRIENDLY_NAME: name,
|
ATTR_FRIENDLY_NAME: name,
|
||||||
|
ATTR_ENTITY_PICTURE: entity_picture,
|
||||||
}
|
}
|
||||||
|
|
||||||
data.update({
|
data.update({
|
||||||
|
@ -60,11 +60,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
add_devices(covers)
|
add_devices(covers)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
|
||||||
class CommandCover(CoverDevice):
|
class CommandCover(CoverDevice):
|
||||||
"""Representation a command line cover."""
|
"""Representation a command line cover."""
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def __init__(self, hass, name, command_open, command_close, command_stop,
|
def __init__(self, hass, name, command_open, command_close, command_stop,
|
||||||
command_state, value_template):
|
command_state, value_template):
|
||||||
"""Initialize the cover."""
|
"""Initialize the cover."""
|
||||||
|
@ -20,7 +20,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
class DemoCover(CoverDevice):
|
class DemoCover(CoverDevice):
|
||||||
"""Representation of a demo cover."""
|
"""Representation of a demo cover."""
|
||||||
|
|
||||||
# pylint: disable=no-self-use, too-many-instance-attributes
|
# pylint: disable=no-self-use
|
||||||
def __init__(self, hass, name, position=None, tilt_position=None):
|
def __init__(self, hass, name, position=None, tilt_position=None):
|
||||||
"""Initialize the cover."""
|
"""Initialize the cover."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
275
homeassistant/components/cover/garadget.py
Normal file
275
homeassistant/components/cover/garadget.py
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
"""
|
||||||
|
Platform for the garadget cover component.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation
|
||||||
|
https://home-assistant.io/components/garadget/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA
|
||||||
|
from homeassistant.helpers.event import track_utc_time_change
|
||||||
|
from homeassistant.const import CONF_DEVICE, CONF_USERNAME, CONF_PASSWORD,\
|
||||||
|
CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN, STATE_CLOSED, STATE_OPEN,\
|
||||||
|
CONF_COVERS
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'Garadget'
|
||||||
|
|
||||||
|
ATTR_SIGNAL_STRENGTH = "wifi signal strength (dB)"
|
||||||
|
ATTR_TIME_IN_STATE = "time in state"
|
||||||
|
ATTR_SENSOR_STRENGTH = "sensor reflection rate"
|
||||||
|
ATTR_AVAILABLE = "available"
|
||||||
|
|
||||||
|
STATE_OPENING = "opening"
|
||||||
|
STATE_CLOSING = "closing"
|
||||||
|
STATE_STOPPED = "stopped"
|
||||||
|
STATE_OFFLINE = "offline"
|
||||||
|
|
||||||
|
STATES_MAP = {
|
||||||
|
"open": STATE_OPEN,
|
||||||
|
"opening": STATE_OPENING,
|
||||||
|
"closed": STATE_CLOSED,
|
||||||
|
"closing": STATE_CLOSING,
|
||||||
|
"stopped": STATE_STOPPED
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Validation of the user's configuration
|
||||||
|
COVER_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(CONF_DEVICE): cv.string,
|
||||||
|
vol.Optional(CONF_USERNAME): cv.string,
|
||||||
|
vol.Optional(CONF_PASSWORD): cv.string,
|
||||||
|
vol.Optional(CONF_ACCESS_TOKEN): cv.string,
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
|
||||||
|
})
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
|
||||||
|
})
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the Demo covers."""
|
||||||
|
covers = []
|
||||||
|
devices = config.get(CONF_COVERS, {})
|
||||||
|
|
||||||
|
_LOGGER.debug(devices)
|
||||||
|
|
||||||
|
for device_id, device_config in devices.items():
|
||||||
|
args = {
|
||||||
|
"name": device_config.get(CONF_NAME),
|
||||||
|
"device_id": device_config.get(CONF_DEVICE, device_id),
|
||||||
|
"username": device_config.get(CONF_USERNAME),
|
||||||
|
"password": device_config.get(CONF_PASSWORD),
|
||||||
|
"access_token": device_config.get(CONF_ACCESS_TOKEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
covers.append(GaradgetCover(hass, args))
|
||||||
|
|
||||||
|
add_devices(covers)
|
||||||
|
|
||||||
|
|
||||||
|
class GaradgetCover(CoverDevice):
|
||||||
|
"""Representation of a demo cover."""
|
||||||
|
|
||||||
|
# pylint: disable=no-self-use, too-many-instance-attributes
|
||||||
|
def __init__(self, hass, args):
|
||||||
|
"""Initialize the cover."""
|
||||||
|
self.particle_url = 'https://api.particle.io'
|
||||||
|
self.hass = hass
|
||||||
|
self._name = args['name']
|
||||||
|
self.device_id = args['device_id']
|
||||||
|
self.access_token = args['access_token']
|
||||||
|
self.obtained_token = False
|
||||||
|
self._username = args['username']
|
||||||
|
self._password = args['password']
|
||||||
|
self._state = STATE_UNKNOWN
|
||||||
|
self.time_in_state = None
|
||||||
|
self.signal = None
|
||||||
|
self.sensor = None
|
||||||
|
self._unsub_listener_cover = None
|
||||||
|
self._available = True
|
||||||
|
|
||||||
|
if self.access_token is None:
|
||||||
|
self.access_token = self.get_token()
|
||||||
|
self._obtained_token = True
|
||||||
|
|
||||||
|
# Lets try to get the configured name if not provided.
|
||||||
|
try:
|
||||||
|
if self._name is None:
|
||||||
|
doorconfig = self._get_variable("doorConfig")
|
||||||
|
if doorconfig["nme"] is not None:
|
||||||
|
self._name = doorconfig["nme"]
|
||||||
|
self.update()
|
||||||
|
except requests.exceptions.ConnectionError as ex:
|
||||||
|
_LOGGER.error('Unable to connect to server: %(reason)s',
|
||||||
|
dict(reason=ex))
|
||||||
|
self._state = STATE_OFFLINE
|
||||||
|
self._available = False
|
||||||
|
self._name = DEFAULT_NAME
|
||||||
|
except KeyError as ex:
|
||||||
|
_LOGGER.warning('Garadget device %(device)s seems to be offline',
|
||||||
|
dict(device=self.device_id))
|
||||||
|
self._name = DEFAULT_NAME
|
||||||
|
self._state = STATE_OFFLINE
|
||||||
|
self._available = False
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""Try to remove token."""
|
||||||
|
if self._obtained_token is True:
|
||||||
|
if self.access_token is not None:
|
||||||
|
self.remove_token()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the cover."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No polling needed for a demo cover."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return self._available
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the device state attributes."""
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
if self.signal is not None:
|
||||||
|
data[ATTR_SIGNAL_STRENGTH] = self.signal
|
||||||
|
|
||||||
|
if self.time_in_state is not None:
|
||||||
|
data[ATTR_TIME_IN_STATE] = self.time_in_state
|
||||||
|
|
||||||
|
if self.sensor is not None:
|
||||||
|
data[ATTR_SENSOR_STRENGTH] = self.sensor
|
||||||
|
|
||||||
|
if self.access_token is not None:
|
||||||
|
data[CONF_ACCESS_TOKEN] = self.access_token
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
"""Return if the cover is closed."""
|
||||||
|
if self._state == STATE_UNKNOWN:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return self._state == STATE_CLOSED
|
||||||
|
|
||||||
|
def get_token(self):
|
||||||
|
"""Get new token for usage during this session."""
|
||||||
|
args = {
|
||||||
|
'grant_type': 'password',
|
||||||
|
'username': self._username,
|
||||||
|
'password': self._password
|
||||||
|
}
|
||||||
|
url = '{}/oauth/token'.format(self.particle_url)
|
||||||
|
ret = requests.post(url,
|
||||||
|
auth=('particle', 'particle'),
|
||||||
|
data=args)
|
||||||
|
|
||||||
|
return ret.json()['access_token']
|
||||||
|
|
||||||
|
def remove_token(self):
|
||||||
|
"""Remove authorization token from API."""
|
||||||
|
ret = requests.delete('{}/v1/access_tokens/{}'.format(
|
||||||
|
self.particle_url,
|
||||||
|
self.access_token),
|
||||||
|
auth=(self._username, self._password))
|
||||||
|
return ret.text
|
||||||
|
|
||||||
|
def _start_watcher(self, command):
|
||||||
|
"""Start watcher."""
|
||||||
|
_LOGGER.debug("Starting Watcher for command: %s ", command)
|
||||||
|
if self._unsub_listener_cover is None:
|
||||||
|
self._unsub_listener_cover = track_utc_time_change(
|
||||||
|
self.hass, self._check_state)
|
||||||
|
|
||||||
|
def _check_state(self, now):
|
||||||
|
"""Check the state of the service during an operation."""
|
||||||
|
self.update()
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
|
def close_cover(self):
|
||||||
|
"""Close the cover."""
|
||||||
|
if self._state not in ["close", "closing"]:
|
||||||
|
ret = self._put_command("setState", "close")
|
||||||
|
self._start_watcher('close')
|
||||||
|
return ret.get('return_value') == 1
|
||||||
|
|
||||||
|
def open_cover(self):
|
||||||
|
"""Open the cover."""
|
||||||
|
if self._state not in ["open", "opening"]:
|
||||||
|
ret = self._put_command("setState", "open")
|
||||||
|
self._start_watcher('open')
|
||||||
|
return ret.get('return_value') == 1
|
||||||
|
|
||||||
|
def stop_cover(self):
|
||||||
|
"""Stop the door where it is."""
|
||||||
|
if self._state not in ["stopped"]:
|
||||||
|
ret = self._put_command("setState", "stop")
|
||||||
|
self._start_watcher('stop')
|
||||||
|
return ret['return_value'] == 1
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Get updated status from API."""
|
||||||
|
try:
|
||||||
|
status = self._get_variable("doorStatus")
|
||||||
|
_LOGGER.debug("Current Status: %s", status['status'])
|
||||||
|
self._state = STATES_MAP.get(status['status'], STATE_UNKNOWN)
|
||||||
|
self.time_in_state = status['time']
|
||||||
|
self.signal = status['signal']
|
||||||
|
self.sensor = status['sensor']
|
||||||
|
self._availble = True
|
||||||
|
except requests.exceptions.ConnectionError as ex:
|
||||||
|
_LOGGER.error('Unable to connect to server: %(reason)s',
|
||||||
|
dict(reason=ex))
|
||||||
|
self._state = STATE_OFFLINE
|
||||||
|
except KeyError as ex:
|
||||||
|
_LOGGER.warning('Garadget device %(device)s seems to be offline',
|
||||||
|
dict(device=self.device_id))
|
||||||
|
self._state = STATE_OFFLINE
|
||||||
|
|
||||||
|
if self._state not in [STATE_CLOSING, STATE_OPENING]:
|
||||||
|
if self._unsub_listener_cover is not None:
|
||||||
|
self._unsub_listener_cover()
|
||||||
|
self._unsub_listener_cover = None
|
||||||
|
|
||||||
|
def _get_variable(self, var):
|
||||||
|
"""Get latest status."""
|
||||||
|
url = '{}/v1/devices/{}/{}?access_token={}'.format(
|
||||||
|
self.particle_url,
|
||||||
|
self.device_id,
|
||||||
|
var,
|
||||||
|
self.access_token,
|
||||||
|
)
|
||||||
|
ret = requests.get(url)
|
||||||
|
result = {}
|
||||||
|
for pairs in ret.json()['result'].split('|'):
|
||||||
|
key = pairs.split('=')
|
||||||
|
result[key[0]] = key[1]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _put_command(self, func, arg=None):
|
||||||
|
"""Send commands to API."""
|
||||||
|
params = {'access_token': self.access_token}
|
||||||
|
if arg:
|
||||||
|
params['command'] = arg
|
||||||
|
url = '{}/v1/devices/{}/{}'.format(
|
||||||
|
self.particle_url,
|
||||||
|
self.device_id,
|
||||||
|
func)
|
||||||
|
ret = requests.post(url, data=params)
|
||||||
|
return ret.json()
|
@ -31,7 +31,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class HMCover(homematic.HMDevice, CoverDevice):
|
class HMCover(homematic.HMDevice, CoverDevice):
|
||||||
"""Represents a Homematic Cover in Home Assistant."""
|
"""Represents a Homematic Cover in Home Assistant."""
|
||||||
|
|
||||||
|
@ -67,7 +67,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
)])
|
)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
|
||||||
class MqttCover(CoverDevice):
|
class MqttCover(CoverDevice):
|
||||||
"""Representation of a cover that can be controlled using MQTT."""
|
"""Representation of a cover that can be controlled using MQTT."""
|
||||||
|
|
||||||
|
@ -40,7 +40,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update)
|
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice):
|
class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice):
|
||||||
"""Representation of an rfxtrx cover."""
|
"""Representation of an rfxtrx cover."""
|
||||||
|
|
||||||
|
@ -63,11 +63,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
add_devices(covers)
|
add_devices(covers)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class RPiGPIOCover(CoverDevice):
|
class RPiGPIOCover(CoverDevice):
|
||||||
"""Representation of a Raspberry GPIO cover."""
|
"""Representation of a Raspberry GPIO cover."""
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def __init__(self, name, relay_pin, state_pin, state_pull_mode,
|
def __init__(self, name, relay_pin, state_pin, state_pull_mode,
|
||||||
relay_time):
|
relay_time):
|
||||||
"""Initialize the cover."""
|
"""Initialize the cover."""
|
||||||
|
@ -45,7 +45,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
add_devices(covers)
|
add_devices(covers)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
|
||||||
class SCSGateCover(CoverDevice):
|
class SCSGateCover(CoverDevice):
|
||||||
"""Representation of SCSGate cover."""
|
"""Representation of SCSGate cover."""
|
||||||
|
|
||||||
|
@ -15,14 +15,13 @@ DEPENDENCIES = ['vera']
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Find and return Vera covers."""
|
"""Find and return Vera covers."""
|
||||||
add_devices_callback(
|
add_devices(
|
||||||
VeraCover(device, VERA_CONTROLLER) for
|
VeraCover(device, VERA_CONTROLLER) for
|
||||||
device in VERA_DEVICES['cover'])
|
device in VERA_DEVICES['cover'])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class VeraCover(VeraDevice, CoverDevice):
|
class VeraCover(VeraDevice, CoverDevice):
|
||||||
"""Represents a Vera Cover in Home Assistant."""
|
"""Represents a Vera Cover in Home Assistant."""
|
||||||
|
|
||||||
|
@ -32,14 +32,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||||
|
|
||||||
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL) \
|
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||||
and value.index == 0:
|
and value.index == 0):
|
||||||
value.set_change_verified(False)
|
value.set_change_verified(False)
|
||||||
add_devices([ZwaveRollershutter(value)])
|
add_devices([ZwaveRollershutter(value)])
|
||||||
elif node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_BINARY) or \
|
elif value.node.specific == zwave.const.GENERIC_TYPE_ENTRY_CONTROL:
|
||||||
node.has_command_class(zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
|
||||||
if value.type != zwave.const.TYPE_BOOL and \
|
value.command_class ==
|
||||||
value.genre != zwave.const.GENRE_USER:
|
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||||
|
if (value.type != zwave.const.TYPE_BOOL and
|
||||||
|
value.genre != zwave.const.GENRE_USER):
|
||||||
return
|
return
|
||||||
value.set_change_verified(False)
|
value.set_change_verified(False)
|
||||||
add_devices([ZwaveGarageDoor(value)])
|
add_devices([ZwaveGarageDoor(value)])
|
||||||
@ -122,7 +124,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
'Open' or value.command_class == \
|
'Open' or value.command_class == \
|
||||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
'Down':
|
'Up':
|
||||||
self._lozwmgr.pressButton(value.value_id)
|
self._lozwmgr.pressButton(value.value_id)
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -132,7 +134,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||||
if value.command_class == \
|
if value.command_class == \
|
||||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
'Up' or value.command_class == \
|
'Down' or value.command_class == \
|
||||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
'Close':
|
'Close':
|
||||||
self._lozwmgr.pressButton(value.value_id)
|
self._lozwmgr.pressButton(value.value_id)
|
||||||
|
@ -41,7 +41,6 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-locals
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""The triggers to turn lights on or off based on device presence."""
|
"""The triggers to turn lights on or off based on device presence."""
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -4,8 +4,6 @@ Provide functionality to keep track of devices.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/device_tracker/
|
https://home-assistant.io/components/device_tracker/
|
||||||
"""
|
"""
|
||||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
|
||||||
# pylint: disable=too-many-locals
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
@ -14,7 +12,6 @@ import threading
|
|||||||
from typing import Any, Sequence, Callable
|
from typing import Any, Sequence, Callable
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import yaml
|
|
||||||
|
|
||||||
from homeassistant.bootstrap import (
|
from homeassistant.bootstrap import (
|
||||||
prepare_setup_platform, log_exception)
|
prepare_setup_platform, log_exception)
|
||||||
@ -29,6 +26,7 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.util.async import run_coroutine_threadsafe
|
from homeassistant.util.async import run_coroutine_threadsafe
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
from homeassistant.util.yaml import dump
|
||||||
|
|
||||||
from homeassistant.helpers.event import track_utc_time_change
|
from homeassistant.helpers.event import track_utc_time_change
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -57,6 +55,8 @@ DEFAULT_SCAN_INTERVAL = 12
|
|||||||
CONF_AWAY_HIDE = 'hide_if_away'
|
CONF_AWAY_HIDE = 'hide_if_away'
|
||||||
DEFAULT_AWAY_HIDE = False
|
DEFAULT_AWAY_HIDE = False
|
||||||
|
|
||||||
|
EVENT_NEW_DEVICE = 'device_tracker_new_device'
|
||||||
|
|
||||||
SERVICE_SEE = 'see'
|
SERVICE_SEE = 'see'
|
||||||
|
|
||||||
ATTR_MAC = 'mac'
|
ATTR_MAC = 'mac'
|
||||||
@ -88,7 +88,6 @@ def is_on(hass: HomeAssistantType, entity_id: str=None):
|
|||||||
return hass.states.is_state(entity, STATE_HOME)
|
return hass.states.is_state(entity, STATE_HOME)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
|
def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
|
||||||
host_name: str=None, location_name: str=None,
|
host_name: str=None, location_name: str=None,
|
||||||
gps: GPSType=None, gps_accuracy=None,
|
gps: GPSType=None, gps_accuracy=None,
|
||||||
@ -103,8 +102,7 @@ def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
|
|||||||
(ATTR_GPS_ACCURACY, gps_accuracy),
|
(ATTR_GPS_ACCURACY, gps_accuracy),
|
||||||
(ATTR_BATTERY, battery)) if value is not None}
|
(ATTR_BATTERY, battery)) if value is not None}
|
||||||
if attributes:
|
if attributes:
|
||||||
for key, value in attributes:
|
data[ATTR_ATTRIBUTES] = attributes
|
||||||
data[key] = value
|
|
||||||
hass.services.call(DOMAIN, SERVICE_SEE, data)
|
hass.services.call(DOMAIN, SERVICE_SEE, data)
|
||||||
|
|
||||||
|
|
||||||
@ -115,7 +113,7 @@ def setup(hass: HomeAssistantType, config: ConfigType):
|
|||||||
try:
|
try:
|
||||||
conf = config.get(DOMAIN, [])
|
conf = config.get(DOMAIN, [])
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
log_exception(ex, DOMAIN, config)
|
log_exception(ex, DOMAIN, config, hass)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
conf = conf[0] if len(conf) > 0 else {}
|
conf = conf[0] if len(conf) > 0 else {}
|
||||||
@ -240,9 +238,12 @@ class DeviceTracker(object):
|
|||||||
|
|
||||||
device.seen(host_name, location_name, gps, gps_accuracy, battery,
|
device.seen(host_name, location_name, gps, gps_accuracy, battery,
|
||||||
attributes)
|
attributes)
|
||||||
|
|
||||||
if device.track:
|
if device.track:
|
||||||
device.update_ha_state()
|
device.update_ha_state()
|
||||||
|
|
||||||
|
self.hass.bus.async_fire(EVENT_NEW_DEVICE, device)
|
||||||
|
|
||||||
# During init, we ignore the group
|
# During init, we ignore the group
|
||||||
if self.group is not None:
|
if self.group is not None:
|
||||||
self.group.update_tracked_entity_ids(
|
self.group.update_tracked_entity_ids(
|
||||||
@ -432,7 +433,7 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
|||||||
device = dev_schema(device)
|
device = dev_schema(device)
|
||||||
device['dev_id'] = cv.slugify(dev_id)
|
device['dev_id'] = cv.slugify(dev_id)
|
||||||
except vol.Invalid as exp:
|
except vol.Invalid as exp:
|
||||||
log_exception(exp, dev_id, devices)
|
log_exception(exp, dev_id, devices, hass)
|
||||||
else:
|
else:
|
||||||
result.append(Device(hass, **device))
|
result.append(Device(hass, **device))
|
||||||
return result
|
return result
|
||||||
@ -468,8 +469,6 @@ def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
|||||||
def update_config(path: str, dev_id: str, device: Device):
|
def update_config(path: str, dev_id: str, device: Device):
|
||||||
"""Add device to YAML configuration file."""
|
"""Add device to YAML configuration file."""
|
||||||
with open(path, 'a') as out:
|
with open(path, 'a') as out:
|
||||||
out.write('\n')
|
|
||||||
|
|
||||||
device = {device.dev_id: {
|
device = {device.dev_id: {
|
||||||
'name': device.name,
|
'name': device.name,
|
||||||
'mac': device.mac,
|
'mac': device.mac,
|
||||||
@ -477,7 +476,8 @@ def update_config(path: str, dev_id: str, device: Device):
|
|||||||
'track': device.track,
|
'track': device.track,
|
||||||
CONF_AWAY_HIDE: device.away_hide
|
CONF_AWAY_HIDE: device.away_hide
|
||||||
}}
|
}}
|
||||||
yaml.dump(device, out, default_flow_style=False)
|
out.write('\n')
|
||||||
|
out.write(dump(device))
|
||||||
|
|
||||||
|
|
||||||
def get_gravatar_for_email(email: str):
|
def get_gravatar_for_email(email: str):
|
||||||
|
@ -90,9 +90,7 @@ AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp')
|
|||||||
class AsusWrtDeviceScanner(object):
|
class AsusWrtDeviceScanner(object):
|
||||||
"""This class queries a router running ASUSWRT firmware."""
|
"""This class queries a router running ASUSWRT firmware."""
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes, too-many-branches
|
|
||||||
# Eighth attribute needed for mode (AP mode vs router mode)
|
# Eighth attribute needed for mode (AP mode vs router mode)
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
"""Initialize the scanner."""
|
"""Initialize the scanner."""
|
||||||
self.host = config[CONF_HOST]
|
self.host = config[CONF_HOST]
|
||||||
|
@ -60,8 +60,6 @@ def setup_scanner(hass, config: dict, see):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
# pylint: disable=too-few-public-methods
|
|
||||||
class AutomaticDeviceScanner(object):
|
class AutomaticDeviceScanner(object):
|
||||||
"""A class representing an Automatic device."""
|
"""A class representing an Automatic device."""
|
||||||
|
|
||||||
|
@ -7,15 +7,16 @@ https://home-assistant.io/components/device_tracker.bbox/
|
|||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.components.device_tracker import DOMAIN
|
from homeassistant.components.device_tracker import DOMAIN
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
# Return cached results if last scan was less then this time ago
|
REQUIREMENTS = ['pybbox==0.0.5-alpha']
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
REQUIREMENTS = ['pybbox==0.0.5-alpha']
|
|
||||||
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60)
|
||||||
|
|
||||||
|
|
||||||
def get_scanner(hass, config):
|
def get_scanner(hass, config):
|
||||||
@ -36,7 +37,7 @@ class BboxDeviceScanner(object):
|
|||||||
self.last_results = [] # type: List[Device]
|
self.last_results = [] # type: List[Device]
|
||||||
|
|
||||||
self.success_init = self._update_info()
|
self.success_init = self._update_info()
|
||||||
_LOGGER.info('Bbox scanner initialized')
|
_LOGGER.info("Bbox scanner initialized")
|
||||||
|
|
||||||
def scan_devices(self):
|
def scan_devices(self):
|
||||||
"""Scan for new devices and return a list with found device IDs."""
|
"""Scan for new devices and return a list with found device IDs."""
|
||||||
@ -60,7 +61,7 @@ class BboxDeviceScanner(object):
|
|||||||
|
|
||||||
Returns boolean if scanning successful.
|
Returns boolean if scanning successful.
|
||||||
"""
|
"""
|
||||||
_LOGGER.info('Scanning')
|
_LOGGER.info("Scanning...")
|
||||||
|
|
||||||
import pybbox
|
import pybbox
|
||||||
|
|
||||||
@ -78,5 +79,5 @@ class BboxDeviceScanner(object):
|
|||||||
|
|
||||||
self.last_results = last_results
|
self.last_results = last_results
|
||||||
|
|
||||||
_LOGGER.info('Bbox scan successful')
|
_LOGGER.info("Bbox scan successful")
|
||||||
return True
|
return True
|
||||||
|
@ -35,12 +35,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def get_scanner(hass, config):
|
def get_scanner(hass, config):
|
||||||
"""Validate the configuration and return a DD-WRT scanner."""
|
"""Validate the configuration and return a DD-WRT scanner."""
|
||||||
scanner = DdWrtDeviceScanner(config[DOMAIN])
|
try:
|
||||||
|
return DdWrtDeviceScanner(config[DOMAIN])
|
||||||
return scanner if scanner.success_init else None
|
except ConnectionError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
class DdWrtDeviceScanner(object):
|
class DdWrtDeviceScanner(object):
|
||||||
"""This class queries a wireless router running DD-WRT firmware."""
|
"""This class queries a wireless router running DD-WRT firmware."""
|
||||||
|
|
||||||
@ -53,13 +53,13 @@ class DdWrtDeviceScanner(object):
|
|||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
|
|
||||||
self.mac2name = {}
|
self.mac2name = {}
|
||||||
|
|
||||||
# Test the router is accessible
|
# Test the router is accessible
|
||||||
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
|
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
|
||||||
data = self.get_ddwrt_data(url)
|
data = self.get_ddwrt_data(url)
|
||||||
self.success_init = data is not None
|
if not data:
|
||||||
|
raise ConnectionError('Cannot connect to DD-Wrt router')
|
||||||
|
|
||||||
def scan_devices(self):
|
def scan_devices(self):
|
||||||
"""Scan for new devices and return a list with found device IDs."""
|
"""Scan for new devices and return a list with found device IDs."""
|
||||||
@ -83,14 +83,15 @@ class DdWrtDeviceScanner(object):
|
|||||||
if not dhcp_leases:
|
if not dhcp_leases:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Remove leading and trailing single quotes.
|
# Remove leading and trailing quotes and spaces
|
||||||
cleaned_str = dhcp_leases.strip().strip('"')
|
cleaned_str = dhcp_leases.replace(
|
||||||
elements = cleaned_str.split('","')
|
"\"", "").replace("\'", "").replace(" ", "")
|
||||||
|
elements = cleaned_str.split(',')
|
||||||
num_clients = int(len(elements) / 5)
|
num_clients = int(len(elements) / 5)
|
||||||
self.mac2name = {}
|
self.mac2name = {}
|
||||||
for idx in range(0, num_clients):
|
for idx in range(0, num_clients):
|
||||||
# This is stupid but the data is a single array
|
# The data is a single array
|
||||||
# every 5 elements represents one hosts, the MAC
|
# every 5 elements represents one host, the MAC
|
||||||
# is the third element and the name is the first.
|
# is the third element and the name is the first.
|
||||||
mac_index = (idx * 5) + 2
|
mac_index = (idx * 5) + 2
|
||||||
if mac_index < len(elements):
|
if mac_index < len(elements):
|
||||||
@ -105,9 +106,6 @@ class DdWrtDeviceScanner(object):
|
|||||||
|
|
||||||
Return boolean if scanning successful.
|
Return boolean if scanning successful.
|
||||||
"""
|
"""
|
||||||
if not self.success_init:
|
|
||||||
return False
|
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
_LOGGER.info('Checking ARP')
|
_LOGGER.info('Checking ARP')
|
||||||
|
|
||||||
@ -123,11 +121,8 @@ class DdWrtDeviceScanner(object):
|
|||||||
if not active_clients:
|
if not active_clients:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# This is really lame, instead of using JSON the DD-WRT UI
|
# The DD-WRT UI uses its own data format and then
|
||||||
# uses its own data format for some reason and then
|
# regex's out values so this is done here too
|
||||||
# regex's out values so I guess I have to do the same,
|
|
||||||
# LAME!!!
|
|
||||||
|
|
||||||
# Remove leading and trailing single quotes.
|
# Remove leading and trailing single quotes.
|
||||||
clean_str = active_clients.strip().strip("'")
|
clean_str = active_clients.strip().strip("'")
|
||||||
elements = clean_str.split("','")
|
elements = clean_str.split("','")
|
||||||
|
@ -38,7 +38,6 @@ def get_scanner(hass, config):
|
|||||||
return scanner if scanner.success_init else None
|
return scanner if scanner.success_init else None
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
class FritzBoxScanner(object):
|
class FritzBoxScanner(object):
|
||||||
"""This class queries a FRITZ!Box router."""
|
"""This class queries a FRITZ!Box router."""
|
||||||
|
|
||||||
|
@ -1,100 +1,427 @@
|
|||||||
"""
|
"""
|
||||||
Support for iCloud connected devices.
|
Platform that supports scanning iCloud.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/device_tracker.icloud/
|
https://home-assistant.io/components/device_tracker.icloud/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import random
|
||||||
|
import os
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME,
|
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||||
EVENT_HOMEASSISTANT_START)
|
from homeassistant.components.device_tracker import (
|
||||||
|
PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT)
|
||||||
|
from homeassistant.components.zone import active_zone
|
||||||
from homeassistant.helpers.event import track_utc_time_change
|
from homeassistant.helpers.event import track_utc_time_change
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
from homeassistant.components.device_tracker import (ENTITY_ID_FORMAT,
|
import homeassistant.util.dt as dt_util
|
||||||
PLATFORM_SCHEMA)
|
from homeassistant.util.location import distance
|
||||||
|
from homeassistant.loader import get_component
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['pyicloud==0.9.1']
|
REQUIREMENTS = ['pyicloud==0.9.1']
|
||||||
|
|
||||||
CONF_INTERVAL = 'interval'
|
CONF_IGNORED_DEVICES = 'ignored_devices'
|
||||||
KEEPALIVE_INTERVAL = 4
|
CONF_ACCOUNTNAME = 'account_name'
|
||||||
|
|
||||||
|
# entity attributes
|
||||||
|
ATTR_ACCOUNTNAME = 'account_name'
|
||||||
|
ATTR_INTERVAL = 'interval'
|
||||||
|
ATTR_DEVICENAME = 'device_name'
|
||||||
|
ATTR_BATTERY = 'battery'
|
||||||
|
ATTR_DISTANCE = 'distance'
|
||||||
|
ATTR_DEVICESTATUS = 'device_status'
|
||||||
|
ATTR_LOWPOWERMODE = 'low_power_mode'
|
||||||
|
ATTR_BATTERYSTATUS = 'battery_status'
|
||||||
|
|
||||||
|
ICLOUDTRACKERS = {}
|
||||||
|
|
||||||
|
_CONFIGURING = {}
|
||||||
|
|
||||||
|
DEVICESTATUSSET = ['features', 'maxMsgChar', 'darkWake', 'fmlyShare',
|
||||||
|
'deviceStatus', 'remoteLock', 'activationLocked',
|
||||||
|
'deviceClass', 'id', 'deviceModel', 'rawDeviceModel',
|
||||||
|
'passcodeLength', 'canWipeAfterLock', 'trackingInfo',
|
||||||
|
'location', 'msg', 'batteryLevel', 'remoteWipe',
|
||||||
|
'thisDevice', 'snd', 'prsId', 'wipeInProgress',
|
||||||
|
'lowPowerMode', 'lostModeEnabled', 'isLocating',
|
||||||
|
'lostModeCapable', 'mesg', 'name', 'batteryStatus',
|
||||||
|
'lockedTimestamp', 'lostTimestamp', 'locationCapable',
|
||||||
|
'deviceDisplayName', 'lostDevice', 'deviceColor',
|
||||||
|
'wipedTimestamp', 'modelDisplayName', 'locationEnabled',
|
||||||
|
'isMac', 'locFoundEnabled']
|
||||||
|
|
||||||
|
DEVICESTATUSCODES = {'200': 'online', '201': 'offline', '203': 'pending',
|
||||||
|
'204': 'unregistered'}
|
||||||
|
|
||||||
|
SERVICE_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]),
|
||||||
|
vol.Optional(ATTR_DEVICENAME): cv.slugify,
|
||||||
|
vol.Optional(ATTR_INTERVAL): cv.positive_int,
|
||||||
|
})
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_USERNAME): vol.Coerce(str),
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Required(CONF_PASSWORD): vol.Coerce(str),
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_INTERVAL, default=8): vol.All(vol.Coerce(int),
|
vol.Optional(ATTR_ACCOUNTNAME): cv.slugify,
|
||||||
vol.Range(min=1))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def setup_scanner(hass, config, see):
|
def setup_scanner(hass, config: dict, see):
|
||||||
"""Set up the iCloud Scanner."""
|
"""Set up the iCloud Scanner."""
|
||||||
from pyicloud import PyiCloudService
|
username = config.get(CONF_USERNAME)
|
||||||
from pyicloud.exceptions import PyiCloudFailedLoginException
|
password = config.get(CONF_PASSWORD)
|
||||||
from pyicloud.exceptions import PyiCloudNoDevicesException
|
account = config.get(CONF_ACCOUNTNAME, slugify(username.partition('@')[0]))
|
||||||
logging.getLogger("pyicloud.base").setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
username = config[CONF_USERNAME]
|
icloudaccount = Icloud(hass, username, password, account, see)
|
||||||
password = config[CONF_PASSWORD]
|
|
||||||
|
|
||||||
try:
|
if icloudaccount.api is not None:
|
||||||
_LOGGER.info('Logging into iCloud Account')
|
ICLOUDTRACKERS[account] = icloudaccount
|
||||||
# Attempt the login to iCloud
|
|
||||||
api = PyiCloudService(username, password, verify=True)
|
else:
|
||||||
except PyiCloudFailedLoginException as error:
|
_LOGGER.error("No ICLOUDTRACKERS added")
|
||||||
_LOGGER.exception('Error logging into iCloud Service: %s', error)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def keep_alive(now):
|
def lost_iphone(call):
|
||||||
"""Keep authenticating iCloud connection.
|
"""Call the lost iphone function if the device is found."""
|
||||||
|
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
|
||||||
|
devicename = call.data.get(ATTR_DEVICENAME)
|
||||||
|
for account in accounts:
|
||||||
|
if account in ICLOUDTRACKERS:
|
||||||
|
ICLOUDTRACKERS[account].lost_iphone(devicename)
|
||||||
|
hass.services.register(DOMAIN, 'icloud_lost_iphone', lost_iphone,
|
||||||
|
schema=SERVICE_SCHEMA)
|
||||||
|
|
||||||
The session timeouts if we are not using it so we
|
def update_icloud(call):
|
||||||
have to re-authenticate & this will send an email.
|
"""Call the update function of an icloud account."""
|
||||||
"""
|
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
|
||||||
api.authenticate()
|
devicename = call.data.get(ATTR_DEVICENAME)
|
||||||
_LOGGER.info("Authenticate against iCloud")
|
for account in accounts:
|
||||||
|
if account in ICLOUDTRACKERS:
|
||||||
|
ICLOUDTRACKERS[account].update_icloud(devicename)
|
||||||
|
hass.services.register(DOMAIN, 'icloud_update', update_icloud,
|
||||||
|
schema=SERVICE_SCHEMA)
|
||||||
|
|
||||||
seen_devices = {}
|
def reset_account_icloud(call):
|
||||||
|
"""Reset an icloud account."""
|
||||||
|
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
|
||||||
|
for account in accounts:
|
||||||
|
if account in ICLOUDTRACKERS:
|
||||||
|
ICLOUDTRACKERS[account].reset_account_icloud()
|
||||||
|
hass.services.register(DOMAIN, 'icloud_reset_account',
|
||||||
|
reset_account_icloud, schema=SERVICE_SCHEMA)
|
||||||
|
|
||||||
|
def setinterval(call):
|
||||||
|
"""Call the update function of an icloud account."""
|
||||||
|
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
|
||||||
|
interval = call.data.get(ATTR_INTERVAL)
|
||||||
|
devicename = call.data.get(ATTR_DEVICENAME)
|
||||||
|
for account in accounts:
|
||||||
|
if account in ICLOUDTRACKERS:
|
||||||
|
ICLOUDTRACKERS[account].setinterval(interval, devicename)
|
||||||
|
|
||||||
|
hass.services.register(DOMAIN, 'icloud_set_interval', setinterval,
|
||||||
|
schema=SERVICE_SCHEMA)
|
||||||
|
|
||||||
|
# Tells the bootstrapper that the component was successfully initialized
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Icloud(object):
|
||||||
|
"""Represent an icloud account in Home Assistant."""
|
||||||
|
|
||||||
|
def __init__(self, hass, username, password, name, see):
|
||||||
|
"""Initialize an iCloud account."""
|
||||||
|
self.hass = hass
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.api = None
|
||||||
|
self.accountname = name
|
||||||
|
self.devices = {}
|
||||||
|
self.seen_devices = {}
|
||||||
|
self._overridestates = {}
|
||||||
|
self._intervals = {}
|
||||||
|
self.see = see
|
||||||
|
|
||||||
|
self._trusted_device = None
|
||||||
|
self._verification_code = None
|
||||||
|
|
||||||
|
self._attrs = {}
|
||||||
|
self._attrs[ATTR_ACCOUNTNAME] = name
|
||||||
|
|
||||||
|
self.reset_account_icloud()
|
||||||
|
|
||||||
|
randomseconds = random.randint(10, 59)
|
||||||
|
track_utc_time_change(
|
||||||
|
self.hass, self.keep_alive,
|
||||||
|
second=randomseconds
|
||||||
|
)
|
||||||
|
|
||||||
|
def reset_account_icloud(self):
|
||||||
|
"""Reset an icloud account."""
|
||||||
|
from pyicloud import PyiCloudService
|
||||||
|
from pyicloud.exceptions import (
|
||||||
|
PyiCloudFailedLoginException, PyiCloudNoDevicesException)
|
||||||
|
|
||||||
|
icloud_dir = self.hass.config.path('icloud')
|
||||||
|
if not os.path.exists(icloud_dir):
|
||||||
|
os.makedirs(icloud_dir)
|
||||||
|
|
||||||
def update_icloud(now):
|
|
||||||
"""Authenticate against iCloud and scan for devices."""
|
|
||||||
try:
|
try:
|
||||||
keep_alive(None)
|
self.api = PyiCloudService(
|
||||||
# Loop through every device registered with the iCloud account
|
self.username, self.password,
|
||||||
for device in api.devices:
|
cookie_directory=icloud_dir,
|
||||||
status = device.status()
|
verify=True)
|
||||||
dev_id = slugify(status['name'].replace(' ', '', 99))
|
except PyiCloudFailedLoginException as error:
|
||||||
|
self.api = None
|
||||||
|
_LOGGER.error('Error logging into iCloud Service: %s', error)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.devices = {}
|
||||||
|
self._overridestates = {}
|
||||||
|
self._intervals = {}
|
||||||
|
for device in self.api.devices:
|
||||||
|
status = device.status(DEVICESTATUSSET)
|
||||||
|
devicename = slugify(status['name'].replace(' ', '', 99))
|
||||||
|
if devicename not in self.devices:
|
||||||
|
self.devices[devicename] = device
|
||||||
|
self._intervals[devicename] = 1
|
||||||
|
self._overridestates[devicename] = None
|
||||||
|
except PyiCloudNoDevicesException:
|
||||||
|
_LOGGER.error('No iCloud Devices found!')
|
||||||
|
|
||||||
|
def icloud_trusted_device_callback(self, callback_data):
|
||||||
|
"""The trusted device is chosen."""
|
||||||
|
self._trusted_device = int(callback_data.get('0', '0'))
|
||||||
|
self._trusted_device = self.api.trusted_devices[self._trusted_device]
|
||||||
|
if self.accountname in _CONFIGURING:
|
||||||
|
request_id = _CONFIGURING.pop(self.accountname)
|
||||||
|
configurator = get_component('configurator')
|
||||||
|
configurator.request_done(request_id)
|
||||||
|
|
||||||
|
def icloud_need_trusted_device(self):
|
||||||
|
"""We need a trusted device."""
|
||||||
|
configurator = get_component('configurator')
|
||||||
|
if self.accountname in _CONFIGURING:
|
||||||
|
return
|
||||||
|
|
||||||
|
devicesstring = ''
|
||||||
|
devices = self.api.trusted_devices
|
||||||
|
for i, device in enumerate(devices):
|
||||||
|
devicesstring += "{}: {};".format(i, device.get('deviceName'))
|
||||||
|
|
||||||
|
_CONFIGURING[self.accountname] = configurator.request_config(
|
||||||
|
self.hass, 'iCloud {}'.format(self.accountname),
|
||||||
|
self.icloud_trusted_device_callback,
|
||||||
|
description=(
|
||||||
|
'Please choose your trusted device by entering'
|
||||||
|
' the index from this list: ' + devicesstring),
|
||||||
|
entity_picture="/static/images/config_icloud.png",
|
||||||
|
submit_caption='Confirm',
|
||||||
|
fields=[{'id': '0'}]
|
||||||
|
)
|
||||||
|
|
||||||
|
def icloud_verification_callback(self, callback_data):
|
||||||
|
"""The trusted device is chosen."""
|
||||||
|
self._verification_code = callback_data.get('0')
|
||||||
|
if self.accountname in _CONFIGURING:
|
||||||
|
request_id = _CONFIGURING.pop(self.accountname)
|
||||||
|
configurator = get_component('configurator')
|
||||||
|
configurator.request_done(request_id)
|
||||||
|
|
||||||
|
def icloud_need_verification_code(self):
|
||||||
|
"""We need a verification code."""
|
||||||
|
configurator = get_component('configurator')
|
||||||
|
if self.accountname in _CONFIGURING:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.api.send_verification_code(self._trusted_device):
|
||||||
|
self._verification_code = 'waiting'
|
||||||
|
|
||||||
|
_CONFIGURING[self.accountname] = configurator.request_config(
|
||||||
|
self.hass, 'iCloud {}'.format(self.accountname),
|
||||||
|
self.icloud_verification_callback,
|
||||||
|
description=('Please enter the validation code:'),
|
||||||
|
entity_picture="/static/images/config_icloud.png",
|
||||||
|
submit_caption='Confirm',
|
||||||
|
fields=[{'code': '0'}]
|
||||||
|
)
|
||||||
|
|
||||||
|
def keep_alive(self, now):
|
||||||
|
"""Keep the api alive."""
|
||||||
|
from pyicloud.exceptions import PyiCloud2FARequiredError
|
||||||
|
|
||||||
|
if self.api is None:
|
||||||
|
self.reset_account_icloud()
|
||||||
|
|
||||||
|
if self.api is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.api.requires_2fa:
|
||||||
|
try:
|
||||||
|
self.api.authenticate()
|
||||||
|
except PyiCloud2FARequiredError:
|
||||||
|
if self._trusted_device is None:
|
||||||
|
self.icloud_need_trusted_device()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._verification_code is None:
|
||||||
|
self.icloud_need_verification_code()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._verification_code == 'waiting':
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.api.validate_verification_code(
|
||||||
|
self._trusted_device, self._verification_code):
|
||||||
|
self._verification_code = None
|
||||||
|
else:
|
||||||
|
self.api.authenticate()
|
||||||
|
|
||||||
|
currentminutes = dt_util.now().hour * 60 + dt_util.now().minute
|
||||||
|
for devicename in self.devices:
|
||||||
|
interval = self._intervals.get(devicename, 1)
|
||||||
|
if ((currentminutes % interval == 0) or
|
||||||
|
(interval > 10 and
|
||||||
|
currentminutes % interval in [2, 4])):
|
||||||
|
self.update_device(devicename)
|
||||||
|
|
||||||
|
def determine_interval(self, devicename, latitude, longitude, battery):
|
||||||
|
"""Calculate new interval."""
|
||||||
|
distancefromhome = None
|
||||||
|
zone_state = self.hass.states.get('zone.home')
|
||||||
|
zone_state_lat = zone_state.attributes['latitude']
|
||||||
|
zone_state_long = zone_state.attributes['longitude']
|
||||||
|
distancefromhome = distance(latitude, longitude, zone_state_lat,
|
||||||
|
zone_state_long)
|
||||||
|
distancefromhome = round(distancefromhome / 1000, 1)
|
||||||
|
|
||||||
|
currentzone = active_zone(self.hass, latitude, longitude)
|
||||||
|
|
||||||
|
if ((currentzone is not None and
|
||||||
|
currentzone == self._overridestates.get(devicename)) or
|
||||||
|
(currentzone is None and
|
||||||
|
self._overridestates.get(devicename) == 'away')):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._overridestates[devicename] = None
|
||||||
|
|
||||||
|
if currentzone is not None:
|
||||||
|
self._intervals[devicename] = 30
|
||||||
|
return
|
||||||
|
|
||||||
|
if distancefromhome is None:
|
||||||
|
return
|
||||||
|
if distancefromhome > 25:
|
||||||
|
self._intervals[devicename] = round(distancefromhome / 2, 0)
|
||||||
|
elif distancefromhome > 10:
|
||||||
|
self._intervals[devicename] = 5
|
||||||
|
else:
|
||||||
|
self._intervals[devicename] = 1
|
||||||
|
if battery is not None and battery <= 33 and distancefromhome > 3:
|
||||||
|
self._intervals[devicename] = self._intervals[devicename] * 2
|
||||||
|
|
||||||
|
def update_device(self, devicename):
|
||||||
|
"""Update the device_tracker entity."""
|
||||||
|
from pyicloud.exceptions import PyiCloudNoDevicesException
|
||||||
|
|
||||||
# An entity will not be created by see() when track=false in
|
# An entity will not be created by see() when track=false in
|
||||||
# 'known_devices.yaml', but we need to see() it at least once
|
# 'known_devices.yaml', but we need to see() it at least once
|
||||||
entity = hass.states.get(ENTITY_ID_FORMAT.format(dev_id))
|
entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename))
|
||||||
if entity is None and dev_id in seen_devices:
|
if entity is None and devicename in self.seen_devices:
|
||||||
|
return
|
||||||
|
attrs = {}
|
||||||
|
kwargs = {}
|
||||||
|
|
||||||
|
if self.api is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
for device in self.api.devices:
|
||||||
|
if str(device) != str(self.devices[devicename]):
|
||||||
continue
|
continue
|
||||||
seen_devices[dev_id] = True
|
|
||||||
|
|
||||||
location = device.location()
|
status = device.status(DEVICESTATUSSET)
|
||||||
# If the device has a location add it. If not do nothing
|
dev_id = status['name'].replace(' ', '', 99)
|
||||||
|
dev_id = slugify(dev_id)
|
||||||
|
attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get(
|
||||||
|
status['deviceStatus'], 'error')
|
||||||
|
attrs[ATTR_LOWPOWERMODE] = status['lowPowerMode']
|
||||||
|
attrs[ATTR_BATTERYSTATUS] = status['batteryStatus']
|
||||||
|
attrs[ATTR_ACCOUNTNAME] = self.accountname
|
||||||
|
status = device.status(DEVICESTATUSSET)
|
||||||
|
battery = status.get('batteryLevel', 0) * 100
|
||||||
|
location = status['location']
|
||||||
if location:
|
if location:
|
||||||
see(
|
self.determine_interval(
|
||||||
dev_id=dev_id,
|
devicename, location['latitude'],
|
||||||
host_name=status['name'],
|
location['longitude'], battery)
|
||||||
gps=(location['latitude'], location['longitude']),
|
interval = self._intervals.get(devicename, 1)
|
||||||
battery=status['batteryLevel']*100,
|
attrs[ATTR_INTERVAL] = interval
|
||||||
gps_accuracy=location['horizontalAccuracy']
|
accuracy = location['horizontalAccuracy']
|
||||||
)
|
kwargs['dev_id'] = dev_id
|
||||||
|
kwargs['host_name'] = status['name']
|
||||||
|
kwargs['gps'] = (location['latitude'],
|
||||||
|
location['longitude'])
|
||||||
|
kwargs['battery'] = battery
|
||||||
|
kwargs['gps_accuracy'] = accuracy
|
||||||
|
kwargs[ATTR_ATTRIBUTES] = attrs
|
||||||
|
self.see(**kwargs)
|
||||||
|
self.seen_devices[devicename] = True
|
||||||
except PyiCloudNoDevicesException:
|
except PyiCloudNoDevicesException:
|
||||||
_LOGGER.info('No iCloud Devices found!')
|
_LOGGER.error('No iCloud Devices found!')
|
||||||
|
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, update_icloud)
|
def lost_iphone(self, devicename):
|
||||||
|
"""Call the lost iphone function if the device is found."""
|
||||||
|
if self.api is None:
|
||||||
|
return
|
||||||
|
|
||||||
update_minutes = list(range(0, 60, config[CONF_INTERVAL]))
|
self.api.authenticate()
|
||||||
# Schedule keepalives between the updates
|
|
||||||
keepalive_minutes = list(x for x in range(0, 60, KEEPALIVE_INTERVAL)
|
|
||||||
if x not in update_minutes)
|
|
||||||
|
|
||||||
track_utc_time_change(hass, update_icloud, second=0, minute=update_minutes)
|
for device in self.api.devices:
|
||||||
track_utc_time_change(hass, keep_alive, second=0, minute=keepalive_minutes)
|
if devicename is None or device == self.devices[devicename]:
|
||||||
|
device.play_sound()
|
||||||
|
|
||||||
return True
|
def update_icloud(self, devicename=None):
|
||||||
|
"""Authenticate against iCloud and scan for devices."""
|
||||||
|
from pyicloud.exceptions import PyiCloudNoDevicesException
|
||||||
|
|
||||||
|
if self.api is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if devicename is not None:
|
||||||
|
if devicename in self.devices:
|
||||||
|
self.devices[devicename].update_icloud()
|
||||||
|
else:
|
||||||
|
_LOGGER.error("devicename %s unknown for account %s",
|
||||||
|
devicename, self._attrs[ATTR_ACCOUNTNAME])
|
||||||
|
else:
|
||||||
|
for device in self.devices:
|
||||||
|
self.devices[device].update_icloud()
|
||||||
|
except PyiCloudNoDevicesException:
|
||||||
|
_LOGGER.error('No iCloud Devices found!')
|
||||||
|
|
||||||
|
def setinterval(self, interval=None, devicename=None):
|
||||||
|
"""Set the interval of the given devices."""
|
||||||
|
devs = [devicename] if devicename else self.devices
|
||||||
|
for device in devs:
|
||||||
|
devid = DOMAIN + '.' + device
|
||||||
|
devicestate = self.hass.states.get(devid)
|
||||||
|
if interval is not None:
|
||||||
|
if devicestate is not None:
|
||||||
|
self._overridestates[device] = active_zone(
|
||||||
|
self.hass,
|
||||||
|
float(devicestate.attributes.get('latitude', 0)),
|
||||||
|
float(devicestate.attributes.get('longitude', 0)))
|
||||||
|
if self._overridestates[device] is None:
|
||||||
|
self._overridestates[device] = 'away'
|
||||||
|
self._intervals[device] = interval
|
||||||
|
else:
|
||||||
|
self._overridestates[device] = None
|
||||||
|
self.update_device(device)
|
||||||
|
@ -4,6 +4,8 @@ Support for the Locative platform.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/device_tracker.locative/
|
https://home-assistant.io/components/device_tracker.locative/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
|
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
|
||||||
@ -19,7 +21,7 @@ DEPENDENCIES = ['http']
|
|||||||
|
|
||||||
def setup_scanner(hass, config, see):
|
def setup_scanner(hass, config, see):
|
||||||
"""Setup an endpoint for the Locative application."""
|
"""Setup an endpoint for the Locative application."""
|
||||||
hass.wsgi.register_view(LocativeView(hass, see))
|
hass.http.register_view(LocativeView(hass, see))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -35,15 +37,23 @@ class LocativeView(HomeAssistantView):
|
|||||||
super().__init__(hass)
|
super().__init__(hass)
|
||||||
self.see = see
|
self.see = see
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Locative message received as GET."""
|
"""Locative message received as GET."""
|
||||||
return self.post(request)
|
res = yield from self._handle(request.GET)
|
||||||
|
return res
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Locative message received."""
|
"""Locative message received."""
|
||||||
# pylint: disable=too-many-return-statements
|
data = yield from request.post()
|
||||||
data = request.values
|
res = yield from self._handle(data)
|
||||||
|
return res
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
# pylint: disable=too-many-return-statements
|
||||||
|
def _handle(self, data):
|
||||||
|
"""Handle locative request."""
|
||||||
if 'latitude' not in data or 'longitude' not in data:
|
if 'latitude' not in data or 'longitude' not in data:
|
||||||
return ('Latitude and longitude not specified.',
|
return ('Latitude and longitude not specified.',
|
||||||
HTTP_UNPROCESSABLE_ENTITY)
|
HTTP_UNPROCESSABLE_ENTITY)
|
||||||
@ -68,7 +78,9 @@ class LocativeView(HomeAssistantView):
|
|||||||
direction = data['trigger']
|
direction = data['trigger']
|
||||||
|
|
||||||
if direction == 'enter':
|
if direction == 'enter':
|
||||||
self.see(dev_id=device, location_name=location_name)
|
yield from self.hass.loop.run_in_executor(
|
||||||
|
None, partial(self.see, dev_id=device,
|
||||||
|
location_name=location_name))
|
||||||
return 'Setting location to {}'.format(location_name)
|
return 'Setting location to {}'.format(location_name)
|
||||||
|
|
||||||
elif direction == 'exit':
|
elif direction == 'exit':
|
||||||
@ -76,7 +88,9 @@ class LocativeView(HomeAssistantView):
|
|||||||
'{}.{}'.format(DOMAIN, device))
|
'{}.{}'.format(DOMAIN, device))
|
||||||
|
|
||||||
if current_state is None or current_state.state == location_name:
|
if current_state is None or current_state.state == location_name:
|
||||||
self.see(dev_id=device, location_name=STATE_NOT_HOME)
|
yield from self.hass.loop.run_in_executor(
|
||||||
|
None, partial(self.see, dev_id=device,
|
||||||
|
location_name=STATE_NOT_HOME))
|
||||||
return 'Setting location to not home'
|
return 'Setting location to not home'
|
||||||
else:
|
else:
|
||||||
# Ignore the message if it is telling us to exit a zone that we
|
# Ignore the message if it is telling us to exit a zone that we
|
||||||
|
@ -37,7 +37,6 @@ def get_scanner(hass, config):
|
|||||||
return scanner if scanner.success_init else None
|
return scanner if scanner.success_init else None
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
class LuciDeviceScanner(object):
|
class LuciDeviceScanner(object):
|
||||||
"""This class queries a wireless router running OpenWrt firmware.
|
"""This class queries a wireless router running OpenWrt firmware.
|
||||||
|
|
||||||
|
@ -18,16 +18,16 @@ from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
|||||||
from homeassistant.const import CONF_HOSTS
|
from homeassistant.const import CONF_HOSTS
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
# Return cached results if last scan was less then this time ago
|
REQUIREMENTS = ['python-nmap==0.6.1']
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_EXCLUDE = 'exclude'
|
||||||
# Interval in minutes to exclude devices from a scan while they are home
|
# Interval in minutes to exclude devices from a scan while they are home
|
||||||
CONF_HOME_INTERVAL = 'home_interval'
|
CONF_HOME_INTERVAL = 'home_interval'
|
||||||
CONF_EXCLUDE = 'exclude'
|
|
||||||
|
|
||||||
REQUIREMENTS = ['python-nmap==0.6.1']
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||||
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOSTS): cv.ensure_list,
|
vol.Required(CONF_HOSTS): cv.ensure_list,
|
||||||
@ -73,7 +73,7 @@ class NmapDeviceScanner(object):
|
|||||||
self.home_interval = timedelta(minutes=minutes)
|
self.home_interval = timedelta(minutes=minutes)
|
||||||
|
|
||||||
self.success_init = self._update_info()
|
self.success_init = self._update_info()
|
||||||
_LOGGER.info('nmap scanner initialized')
|
_LOGGER.info("nmap scanner initialized")
|
||||||
|
|
||||||
def scan_devices(self):
|
def scan_devices(self):
|
||||||
"""Scan for new devices and return a list with found device IDs."""
|
"""Scan for new devices and return a list with found device IDs."""
|
||||||
@ -97,7 +97,7 @@ class NmapDeviceScanner(object):
|
|||||||
|
|
||||||
Returns boolean if scanning successful.
|
Returns boolean if scanning successful.
|
||||||
"""
|
"""
|
||||||
_LOGGER.info('Scanning')
|
_LOGGER.info("Scanning...")
|
||||||
|
|
||||||
from nmap import PortScanner, PortScannerError
|
from nmap import PortScanner, PortScannerError
|
||||||
scanner = PortScanner()
|
scanner = PortScanner()
|
||||||
@ -138,5 +138,5 @@ class NmapDeviceScanner(object):
|
|||||||
|
|
||||||
self.last_results = last_results
|
self.last_results = last_results
|
||||||
|
|
||||||
_LOGGER.info('nmap scan successful')
|
_LOGGER.info("nmap scan successful")
|
||||||
return True
|
return True
|
||||||
|
@ -114,10 +114,9 @@ def setup_scanner(hass, config, see):
|
|||||||
'for topic %s.', topic)
|
'for topic %s.', topic)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# pylint: disable=too-many-return-statements
|
||||||
def validate_payload(topic, payload, data_type):
|
def validate_payload(topic, payload, data_type):
|
||||||
"""Validate the OwnTracks payload."""
|
"""Validate the OwnTracks payload."""
|
||||||
# pylint: disable=too-many-return-statements
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(payload)
|
data = json.loads(payload)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -143,7 +142,7 @@ def setup_scanner(hass, config, see):
|
|||||||
return data
|
return data
|
||||||
if max_gps_accuracy is not None and \
|
if max_gps_accuracy is not None and \
|
||||||
convert(data.get('acc'), float, 0.0) > max_gps_accuracy:
|
convert(data.get('acc'), float, 0.0) > max_gps_accuracy:
|
||||||
_LOGGER.warning('Ignoring %s update because expected GPS '
|
_LOGGER.info('Ignoring %s update because expected GPS '
|
||||||
'accuracy %s is not met: %s',
|
'accuracy %s is not met: %s',
|
||||||
data_type, max_gps_accuracy, payload)
|
data_type, max_gps_accuracy, payload)
|
||||||
return None
|
return None
|
||||||
@ -248,7 +247,7 @@ def setup_scanner(hass, config, see):
|
|||||||
if (max_gps_accuracy is not None and
|
if (max_gps_accuracy is not None and
|
||||||
data['acc'] > max_gps_accuracy):
|
data['acc'] > max_gps_accuracy):
|
||||||
valid_gps = False
|
valid_gps = False
|
||||||
_LOGGER.warning(
|
_LOGGER.info(
|
||||||
'Ignoring GPS in region exit because expected '
|
'Ignoring GPS in region exit because expected '
|
||||||
'GPS accuracy %s is not met: %s',
|
'GPS accuracy %s is not met: %s',
|
||||||
max_gps_accuracy, payload)
|
max_gps_accuracy, payload)
|
||||||
|
@ -31,3 +31,48 @@ see:
|
|||||||
battery:
|
battery:
|
||||||
description: Battery level of device
|
description: Battery level of device
|
||||||
example: '100'
|
example: '100'
|
||||||
|
|
||||||
|
icloud:
|
||||||
|
icloud_lost_iphone:
|
||||||
|
description: Service to play the lost iphone sound on an iDevice
|
||||||
|
|
||||||
|
fields:
|
||||||
|
account_name:
|
||||||
|
description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts.
|
||||||
|
example: 'bart'
|
||||||
|
device_name:
|
||||||
|
description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account.
|
||||||
|
example: 'iphonebart'
|
||||||
|
|
||||||
|
icloud_set_interval:
|
||||||
|
description: Service to set the interval of an iDevice
|
||||||
|
|
||||||
|
fields:
|
||||||
|
account_name:
|
||||||
|
description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts.
|
||||||
|
example: 'bart'
|
||||||
|
device_name:
|
||||||
|
description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account.
|
||||||
|
example: 'iphonebart'
|
||||||
|
interval:
|
||||||
|
description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state.
|
||||||
|
example: 1
|
||||||
|
|
||||||
|
icloud_update:
|
||||||
|
description: Service to ask for an update of an iDevice.
|
||||||
|
|
||||||
|
fields:
|
||||||
|
account_name:
|
||||||
|
description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts.
|
||||||
|
example: 'bart'
|
||||||
|
device_name:
|
||||||
|
description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account.
|
||||||
|
example: 'iphonebart'
|
||||||
|
|
||||||
|
icloud_reset_account:
|
||||||
|
description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device.
|
||||||
|
|
||||||
|
fields:
|
||||||
|
account_name:
|
||||||
|
description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts.
|
||||||
|
example: 'bart'
|
||||||
|
@ -49,7 +49,6 @@ def get_scanner(hass, config):
|
|||||||
class SnmpScanner(object):
|
class SnmpScanner(object):
|
||||||
"""Queries any SNMP capable Access Point for connected devices."""
|
"""Queries any SNMP capable Access Point for connected devices."""
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
"""Initialize the scanner."""
|
"""Initialize the scanner."""
|
||||||
from pysnmp.entity.rfc3413.oneliner import cmdgen
|
from pysnmp.entity.rfc3413.oneliner import cmdgen
|
||||||
|
@ -37,7 +37,6 @@ def get_scanner(hass, config):
|
|||||||
return scanner if scanner.success_init else None
|
return scanner if scanner.success_init else None
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
class UbusDeviceScanner(object):
|
class UbusDeviceScanner(object):
|
||||||
"""
|
"""
|
||||||
This class queries a wireless router running OpenWrt firmware.
|
This class queries a wireless router running OpenWrt firmware.
|
||||||
|
@ -7,9 +7,7 @@ https://home-assistant.io/components/device_tracker.volvooncall/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from urllib.parse import urljoin
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import requests
|
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.event import track_point_in_utc_time
|
from homeassistant.helpers.event import track_point_in_utc_time
|
||||||
@ -27,10 +25,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(minutes=1)
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SERVICE_URL = 'https://vocapi.wirelesscar.net/customerapi/rest/v3.0/'
|
REQUIREMENTS = ['volvooncall==0.1.1']
|
||||||
HEADERS = {"X-Device-Id": "Device",
|
|
||||||
"X-OS-Type": "Android",
|
|
||||||
"X-Originator-Type": "App"}
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
@ -40,62 +35,62 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
|
|
||||||
def setup_scanner(hass, config, see):
|
def setup_scanner(hass, config, see):
|
||||||
"""Validate the configuration and return a scanner."""
|
"""Validate the configuration and return a scanner."""
|
||||||
session = requests.Session()
|
from volvooncall import Connection
|
||||||
session.headers.update(HEADERS)
|
connection = Connection(
|
||||||
session.auth = (config.get(CONF_USERNAME),
|
config.get(CONF_USERNAME),
|
||||||
config.get(CONF_PASSWORD))
|
config.get(CONF_PASSWORD))
|
||||||
|
|
||||||
interval = max(MIN_TIME_BETWEEN_SCANS.seconds,
|
interval = max(MIN_TIME_BETWEEN_SCANS.seconds,
|
||||||
config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL))
|
config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL))
|
||||||
|
|
||||||
def query(ref, rel=SERVICE_URL):
|
def _see_vehicle(vehicle):
|
||||||
"""Perform a query to the online service."""
|
position = vehicle["position"]
|
||||||
url = urljoin(rel, ref)
|
dev_id = "volvo_" + slugify(vehicle["registrationNumber"])
|
||||||
_LOGGER.debug("Request for %s", url)
|
host_name = "%s (%s/%s)" % (
|
||||||
res = session.get(url, timeout=15)
|
vehicle["registrationNumber"],
|
||||||
res.raise_for_status()
|
vehicle["vehicleType"],
|
||||||
_LOGGER.debug("Received %s", res.json())
|
vehicle["modelYear"])
|
||||||
return res.json()
|
|
||||||
|
def any_opened(door):
|
||||||
|
"""True if any door/window is opened."""
|
||||||
|
return any([door[key] for key in door if "Open" in key])
|
||||||
|
|
||||||
|
see(dev_id=dev_id,
|
||||||
|
host_name=host_name,
|
||||||
|
gps=(position["latitude"],
|
||||||
|
position["longitude"]),
|
||||||
|
attributes=dict(
|
||||||
|
unlocked=not vehicle["carLocked"],
|
||||||
|
tank_volume=vehicle["fuelTankVolume"],
|
||||||
|
average_fuel_consumption=round(
|
||||||
|
vehicle["averageFuelConsumption"] / 10, 1), # l/100km
|
||||||
|
washer_fluid_low=vehicle["washerFluidLevel"] != "Normal",
|
||||||
|
brake_fluid_low=vehicle["brakeFluid"] != "Normal",
|
||||||
|
service_warning=vehicle["serviceWarningStatus"] != "Normal",
|
||||||
|
bulb_failures=len(vehicle["bulbFailures"]) > 0,
|
||||||
|
doors_open=any_opened(vehicle["doors"]),
|
||||||
|
windows_open=any_opened(vehicle["windows"]),
|
||||||
|
heater_on=vehicle["heater"]["status"] != "off",
|
||||||
|
fuel=vehicle["fuelAmount"],
|
||||||
|
odometer=round(vehicle["odometer"] / 1000), # km
|
||||||
|
range=vehicle["distanceToEmpty"]))
|
||||||
|
|
||||||
def update(now):
|
def update(now):
|
||||||
"""Update status from the online service."""
|
"""Update status from the online service."""
|
||||||
|
_LOGGER.info("Updating")
|
||||||
try:
|
try:
|
||||||
_LOGGER.debug("Updating")
|
res, vehicles = connection.update()
|
||||||
status = query("status", vehicle_url)
|
if not res:
|
||||||
position = query("position", vehicle_url)
|
_LOGGER.error("Could not query server")
|
||||||
see(dev_id=dev_id,
|
return False
|
||||||
host_name=host_name,
|
|
||||||
gps=(position["position"]["latitude"],
|
for vehicle in vehicles:
|
||||||
position["position"]["longitude"]),
|
_see_vehicle(vehicle)
|
||||||
attributes=dict(
|
|
||||||
tank_volume=attributes["fuelTankVolume"],
|
return True
|
||||||
washer_fluid=status["washerFluidLevel"],
|
|
||||||
brake_fluid=status["brakeFluid"],
|
|
||||||
service_warning=status["serviceWarningStatus"],
|
|
||||||
fuel=status["fuelAmount"],
|
|
||||||
odometer=status["odometer"],
|
|
||||||
range=status["distanceToEmpty"]))
|
|
||||||
except requests.exceptions.RequestException as error:
|
|
||||||
_LOGGER.error("Could not query server: %s", error)
|
|
||||||
finally:
|
finally:
|
||||||
track_point_in_utc_time(hass, update,
|
track_point_in_utc_time(hass, update,
|
||||||
now + timedelta(seconds=interval))
|
now + timedelta(seconds=interval))
|
||||||
|
|
||||||
try:
|
|
||||||
_LOGGER.info('Logging in to service')
|
_LOGGER.info('Logging in to service')
|
||||||
user = query("customeraccounts")
|
return update(utcnow())
|
||||||
rel = query(user["accountVehicleRelations"][0])
|
|
||||||
vehicle_url = rel["vehicle"] + '/'
|
|
||||||
attributes = query("attributes", vehicle_url)
|
|
||||||
|
|
||||||
dev_id = "volvo_" + slugify(attributes["registrationNumber"])
|
|
||||||
host_name = "%s %s/%s" % (attributes["registrationNumber"],
|
|
||||||
attributes["vehicleType"],
|
|
||||||
attributes["modelYear"])
|
|
||||||
update(utcnow())
|
|
||||||
return True
|
|
||||||
except requests.exceptions.RequestException as error:
|
|
||||||
_LOGGER.error("Could not log in to service. "
|
|
||||||
"Please check configuration: "
|
|
||||||
"%s", error)
|
|
||||||
return False
|
|
||||||
|
@ -42,7 +42,6 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument,too-few-public-methods
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Set up the Digital Ocean component."""
|
"""Set up the Digital Ocean component."""
|
||||||
conf = config[DOMAIN]
|
conf = config[DOMAIN]
|
||||||
|
@ -14,7 +14,7 @@ import voluptuous as vol
|
|||||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||||
from homeassistant.helpers.discovery import load_platform, discover
|
from homeassistant.helpers.discovery import load_platform, discover
|
||||||
|
|
||||||
REQUIREMENTS = ['netdisco==0.7.2']
|
REQUIREMENTS = ['netdisco==0.7.5']
|
||||||
|
|
||||||
DOMAIN = 'discovery'
|
DOMAIN = 'discovery'
|
||||||
|
|
||||||
@ -33,6 +33,7 @@ SERVICE_HANDLERS = {
|
|||||||
'plex_mediaserver': ('media_player', 'plex'),
|
'plex_mediaserver': ('media_player', 'plex'),
|
||||||
'roku': ('media_player', 'roku'),
|
'roku': ('media_player', 'roku'),
|
||||||
'sonos': ('media_player', 'sonos'),
|
'sonos': ('media_player', 'sonos'),
|
||||||
|
'yamaha': ('media_player', 'yamaha'),
|
||||||
'logitech_mediaserver': ('media_player', 'squeezebox'),
|
'logitech_mediaserver': ('media_player', 'squeezebox'),
|
||||||
'directv': ('media_player', 'directv'),
|
'directv': ('media_player', 'directv'),
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,6 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Listen for download events to download files."""
|
"""Listen for download events to download files."""
|
||||||
download_path = config[DOMAIN][CONF_DOWNLOAD_DIR]
|
download_path = config[DOMAIN][CONF_DOWNLOAD_DIR]
|
||||||
|
@ -32,7 +32,6 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-locals
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup the Dweet.io component."""
|
"""Setup the Dweet.io component."""
|
||||||
conf = config[DOMAIN]
|
conf = config[DOMAIN]
|
||||||
|
@ -86,7 +86,6 @@ def setup_ecobee(hass, network, config):
|
|||||||
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
|
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
|
||||||
class EcobeeData(object):
|
class EcobeeData(object):
|
||||||
"""Get the latest data and update the states."""
|
"""Get the latest data and update the states."""
|
||||||
|
|
||||||
|
@ -11,9 +11,7 @@ import voluptuous as vol
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY, CONF_WHITELIST,
|
CONF_API_KEY, CONF_WHITELIST, CONF_URL, STATE_UNKNOWN, STATE_UNAVAILABLE,
|
||||||
CONF_URL, STATE_UNKNOWN,
|
|
||||||
STATE_UNAVAILABLE,
|
|
||||||
CONF_SCAN_INTERVAL)
|
CONF_SCAN_INTERVAL)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers import state as state_helper
|
from homeassistant.helpers import state as state_helper
|
||||||
@ -22,8 +20,8 @@ from homeassistant.util import dt as dt_util
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "emoncms_history"
|
DOMAIN = 'emoncms_history'
|
||||||
CONF_INPUTNODE = "inputnode"
|
CONF_INPUTNODE = 'inputnode'
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
@ -37,19 +35,18 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup the emoncms_history component."""
|
"""Set up the Emoncms history component."""
|
||||||
conf = config[DOMAIN]
|
conf = config[DOMAIN]
|
||||||
whitelist = conf.get(CONF_WHITELIST)
|
whitelist = conf.get(CONF_WHITELIST)
|
||||||
|
|
||||||
def send_data(url, apikey, node, payload):
|
def send_data(url, apikey, node, payload):
|
||||||
"""Send payload data to emoncms."""
|
"""Send payload data to Emoncms."""
|
||||||
try:
|
try:
|
||||||
fullurl = "{}/input/post.json".format(url)
|
fullurl = '{}/input/post.json'.format(url)
|
||||||
req = requests.post(fullurl,
|
data = {"apikey": apikey, "data": payload}
|
||||||
params={"node": node},
|
parameters = {"node": node}
|
||||||
data={"apikey": apikey,
|
req = requests.post(
|
||||||
"data": payload},
|
fullurl, params=parameters, data=data, allow_redirects=True,
|
||||||
allow_redirects=True,
|
|
||||||
timeout=5)
|
timeout=5)
|
||||||
|
|
||||||
except requests.exceptions.RequestException:
|
except requests.exceptions.RequestException:
|
||||||
@ -63,14 +60,14 @@ def setup(hass, config):
|
|||||||
fullurl, req.status_code)
|
fullurl, req.status_code)
|
||||||
|
|
||||||
def update_emoncms(time):
|
def update_emoncms(time):
|
||||||
"""Send whitelisted entities states reguarly to emoncms."""
|
"""Send whitelisted entities states reguarly to Emoncms."""
|
||||||
payload_dict = {}
|
payload_dict = {}
|
||||||
|
|
||||||
for entity_id in whitelist:
|
for entity_id in whitelist:
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
|
|
||||||
if state is None or state.state in (
|
if state is None or state.state in (
|
||||||
STATE_UNKNOWN, "", STATE_UNAVAILABLE):
|
STATE_UNKNOWN, '', STATE_UNAVAILABLE):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -88,8 +85,7 @@ def setup(hass, config):
|
|||||||
str(conf.get(CONF_INPUTNODE)), payload)
|
str(conf.get(CONF_INPUTNODE)), payload)
|
||||||
|
|
||||||
track_point_in_time(hass, update_emoncms, time +
|
track_point_in_time(hass, update_emoncms, time +
|
||||||
timedelta(seconds=conf.get(
|
timedelta(seconds=conf.get(CONF_SCAN_INTERVAL)))
|
||||||
CONF_SCAN_INTERVAL)))
|
|
||||||
|
|
||||||
update_emoncms(dt_util.utcnow())
|
update_emoncms(dt_util.utcnow())
|
||||||
return True
|
return True
|
||||||
|
@ -4,20 +4,21 @@ Support for local control of entities by emulating the Phillips Hue bridge.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/emulated_hue/
|
https://home-assistant.io/components/emulated_hue/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import threading
|
import threading
|
||||||
import socket
|
import socket
|
||||||
import logging
|
import logging
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import util, core
|
from homeassistant import util, core
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||||
STATE_ON, HTTP_BAD_REQUEST
|
STATE_ON, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||||
)
|
)
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
||||||
@ -25,8 +26,6 @@ from homeassistant.components.light import (
|
|||||||
from homeassistant.components.http import (
|
from homeassistant.components.http import (
|
||||||
HomeAssistantView, HomeAssistantWSGI
|
HomeAssistantView, HomeAssistantWSGI
|
||||||
)
|
)
|
||||||
# pylint: disable=unused-import
|
|
||||||
from homeassistant.components.http import REQUIREMENTS # noqa
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DOMAIN = 'emulated_hue'
|
DOMAIN = 'emulated_hue'
|
||||||
@ -87,24 +86,25 @@ def setup(hass, yaml_config):
|
|||||||
upnp_listener = UPNPResponderThread(
|
upnp_listener = UPNPResponderThread(
|
||||||
config.host_ip_addr, config.listen_port)
|
config.host_ip_addr, config.listen_port)
|
||||||
|
|
||||||
def start_emulated_hue_bridge(event):
|
@asyncio.coroutine
|
||||||
"""Start the emulated hue bridge."""
|
|
||||||
server.start()
|
|
||||||
upnp_listener.start()
|
|
||||||
|
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge)
|
|
||||||
|
|
||||||
def stop_emulated_hue_bridge(event):
|
def stop_emulated_hue_bridge(event):
|
||||||
"""Stop the emulated hue bridge."""
|
"""Stop the emulated hue bridge."""
|
||||||
upnp_listener.stop()
|
upnp_listener.stop()
|
||||||
server.stop()
|
yield from server.stop()
|
||||||
|
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge)
|
@asyncio.coroutine
|
||||||
|
def start_emulated_hue_bridge(event):
|
||||||
|
"""Start the emulated hue bridge."""
|
||||||
|
upnp_listener.start()
|
||||||
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||||
|
stop_emulated_hue_bridge)
|
||||||
|
yield from server.start()
|
||||||
|
|
||||||
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
"""Holds configuration variables for the emulated hue bridge."""
|
"""Holds configuration variables for the emulated hue bridge."""
|
||||||
|
|
||||||
@ -158,6 +158,7 @@ class DescriptionXmlView(HomeAssistantView):
|
|||||||
super().__init__(hass)
|
super().__init__(hass)
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
|
@core.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Handle a GET request."""
|
"""Handle a GET request."""
|
||||||
xml_template = """<?xml version="1.0" encoding="UTF-8" ?>
|
xml_template = """<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
@ -185,7 +186,7 @@ class DescriptionXmlView(HomeAssistantView):
|
|||||||
resp_text = xml_template.format(
|
resp_text = xml_template.format(
|
||||||
self.config.host_ip_addr, self.config.listen_port)
|
self.config.host_ip_addr, self.config.listen_port)
|
||||||
|
|
||||||
return self.Response(resp_text, mimetype='text/xml')
|
return web.Response(text=resp_text, content_type='text/xml')
|
||||||
|
|
||||||
|
|
||||||
class HueUsernameView(HomeAssistantView):
|
class HueUsernameView(HomeAssistantView):
|
||||||
@ -200,9 +201,13 @@ class HueUsernameView(HomeAssistantView):
|
|||||||
"""Initialize the instance of the view."""
|
"""Initialize the instance of the view."""
|
||||||
super().__init__(hass)
|
super().__init__(hass)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Handle a POST request."""
|
"""Handle a POST request."""
|
||||||
data = request.json
|
try:
|
||||||
|
data = yield from request.json()
|
||||||
|
except ValueError:
|
||||||
|
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
if 'devicetype' not in data:
|
if 'devicetype' not in data:
|
||||||
return self.json_message('devicetype not specified',
|
return self.json_message('devicetype not specified',
|
||||||
@ -214,10 +219,10 @@ class HueUsernameView(HomeAssistantView):
|
|||||||
class HueLightsView(HomeAssistantView):
|
class HueLightsView(HomeAssistantView):
|
||||||
"""Handle requests for getting and setting info about entities."""
|
"""Handle requests for getting and setting info about entities."""
|
||||||
|
|
||||||
url = '/api/<username>/lights'
|
url = '/api/{username}/lights'
|
||||||
name = 'api:username:lights'
|
name = 'api:username:lights'
|
||||||
extra_urls = ['/api/<username>/lights/<entity_id>',
|
extra_urls = ['/api/{username}/lights/{entity_id}',
|
||||||
'/api/<username>/lights/<entity_id>/state']
|
'/api/{username}/lights/{entity_id}/state']
|
||||||
requires_auth = False
|
requires_auth = False
|
||||||
|
|
||||||
def __init__(self, hass, config):
|
def __init__(self, hass, config):
|
||||||
@ -226,58 +231,51 @@ class HueLightsView(HomeAssistantView):
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.cached_states = {}
|
self.cached_states = {}
|
||||||
|
|
||||||
|
@core.callback
|
||||||
def get(self, request, username, entity_id=None):
|
def get(self, request, username, entity_id=None):
|
||||||
"""Handle a GET request."""
|
"""Handle a GET request."""
|
||||||
if entity_id is None:
|
if entity_id is None:
|
||||||
return self.get_lights_list()
|
return self.async_get_lights_list()
|
||||||
|
|
||||||
if not request.base_url.endswith('state'):
|
if not request.path.endswith('state'):
|
||||||
return self.get_light_state(entity_id)
|
return self.async_get_light_state(entity_id)
|
||||||
|
|
||||||
return self.Response("Method not allowed", status=405)
|
return web.Response(text="Method not allowed", status=405)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def put(self, request, username, entity_id=None):
|
def put(self, request, username, entity_id=None):
|
||||||
"""Handle a PUT request."""
|
"""Handle a PUT request."""
|
||||||
if not request.base_url.endswith('state'):
|
if not request.path.endswith('state'):
|
||||||
return self.Response("Method not allowed", status=405)
|
return web.Response(text="Method not allowed", status=405)
|
||||||
|
|
||||||
content_type = request.environ.get('CONTENT_TYPE', '')
|
if entity_id and self.hass.states.get(entity_id) is None:
|
||||||
if content_type == 'application/x-www-form-urlencoded':
|
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||||
# Alexa sends JSON data with a form data content type, for
|
|
||||||
# whatever reason, and Werkzeug parses form data automatically,
|
|
||||||
# so we need to do some gymnastics to get the data we need
|
|
||||||
json_data = None
|
|
||||||
|
|
||||||
for key in request.form:
|
|
||||||
try:
|
try:
|
||||||
json_data = json.loads(key)
|
json_data = yield from request.json()
|
||||||
break
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Try the next key?
|
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
|
||||||
pass
|
|
||||||
|
|
||||||
if json_data is None:
|
result = yield from self.async_put_light_state(json_data, entity_id)
|
||||||
return self.Response("Bad request", status=400)
|
return result
|
||||||
else:
|
|
||||||
json_data = request.json
|
|
||||||
|
|
||||||
return self.put_light_state(json_data, entity_id)
|
@core.callback
|
||||||
|
def async_get_lights_list(self):
|
||||||
def get_lights_list(self):
|
|
||||||
"""Process a request to get the list of available lights."""
|
"""Process a request to get the list of available lights."""
|
||||||
json_response = {}
|
json_response = {}
|
||||||
|
|
||||||
for entity in self.hass.states.all():
|
for entity in self.hass.states.async_all():
|
||||||
if self.is_entity_exposed(entity):
|
if self.is_entity_exposed(entity):
|
||||||
json_response[entity.entity_id] = entity_to_json(entity)
|
json_response[entity.entity_id] = entity_to_json(entity)
|
||||||
|
|
||||||
return self.json(json_response)
|
return self.json(json_response)
|
||||||
|
|
||||||
def get_light_state(self, entity_id):
|
@core.callback
|
||||||
|
def async_get_light_state(self, entity_id):
|
||||||
"""Process a request to get the state of an individual light."""
|
"""Process a request to get the state of an individual light."""
|
||||||
entity = self.hass.states.get(entity_id)
|
entity = self.hass.states.get(entity_id)
|
||||||
if entity is None or not self.is_entity_exposed(entity):
|
if entity is None or not self.is_entity_exposed(entity):
|
||||||
return self.Response("Entity not found", status=404)
|
return web.Response(text="Entity not found", status=404)
|
||||||
|
|
||||||
cached_state = self.cached_states.get(entity_id, None)
|
cached_state = self.cached_states.get(entity_id, None)
|
||||||
|
|
||||||
@ -292,23 +290,24 @@ class HueLightsView(HomeAssistantView):
|
|||||||
|
|
||||||
return self.json(json_response)
|
return self.json(json_response)
|
||||||
|
|
||||||
def put_light_state(self, request_json, entity_id):
|
@asyncio.coroutine
|
||||||
|
def async_put_light_state(self, request_json, entity_id):
|
||||||
"""Process a request to set the state of an individual light."""
|
"""Process a request to set the state of an individual light."""
|
||||||
config = self.config
|
config = self.config
|
||||||
|
|
||||||
# Retrieve the entity from the state machine
|
# Retrieve the entity from the state machine
|
||||||
entity = self.hass.states.get(entity_id)
|
entity = self.hass.states.get(entity_id)
|
||||||
if entity is None:
|
if entity is None:
|
||||||
return self.Response("Entity not found", status=404)
|
return web.Response(text="Entity not found", status=404)
|
||||||
|
|
||||||
if not self.is_entity_exposed(entity):
|
if not self.is_entity_exposed(entity):
|
||||||
return self.Response("Entity not found", status=404)
|
return web.Response(text="Entity not found", status=404)
|
||||||
|
|
||||||
# Parse the request into requested "on" status and brightness
|
# Parse the request into requested "on" status and brightness
|
||||||
parsed = parse_hue_api_put_light_body(request_json, entity)
|
parsed = parse_hue_api_put_light_body(request_json, entity)
|
||||||
|
|
||||||
if parsed is None:
|
if parsed is None:
|
||||||
return self.Response("Bad request", status=400)
|
return web.Response(text="Bad request", status=400)
|
||||||
|
|
||||||
result, brightness = parsed
|
result, brightness = parsed
|
||||||
|
|
||||||
@ -333,7 +332,8 @@ class HueLightsView(HomeAssistantView):
|
|||||||
self.cached_states[entity_id] = (result, brightness)
|
self.cached_states[entity_id] = (result, brightness)
|
||||||
|
|
||||||
# Perform the requested action
|
# Perform the requested action
|
||||||
self.hass.services.call(core.DOMAIN, service, data, blocking=True)
|
yield from self.hass.services.async_call(core.DOMAIN, service, data,
|
||||||
|
blocking=True)
|
||||||
|
|
||||||
json_response = \
|
json_response = \
|
||||||
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
|
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
|
||||||
@ -345,7 +345,10 @@ class HueLightsView(HomeAssistantView):
|
|||||||
return self.json(json_response)
|
return self.json(json_response)
|
||||||
|
|
||||||
def is_entity_exposed(self, entity):
|
def is_entity_exposed(self, entity):
|
||||||
"""Determine if an entity should be exposed on the emulated bridge."""
|
"""Determine if an entity should be exposed on the emulated bridge.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
config = self.config
|
config = self.config
|
||||||
|
|
||||||
if entity.attributes.get('view') is not None:
|
if entity.attributes.get('view') is not None:
|
||||||
|
@ -56,14 +56,14 @@ class EnOceanDongle:
|
|||||||
"""Send a command from the EnOcean dongle."""
|
"""Send a command from the EnOcean dongle."""
|
||||||
self.__communicator.send(command)
|
self.__communicator.send(command)
|
||||||
|
|
||||||
def _combine_hex(self, data): # pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
|
def _combine_hex(self, data):
|
||||||
"""Combine list of integer values to one big integer."""
|
"""Combine list of integer values to one big integer."""
|
||||||
output = 0x00
|
output = 0x00
|
||||||
for i, j in enumerate(reversed(data)):
|
for i, j in enumerate(reversed(data)):
|
||||||
output |= (j << i * 8)
|
output |= (j << i * 8)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
|
||||||
def callback(self, temp):
|
def callback(self, temp):
|
||||||
"""Callback function for EnOcean Device.
|
"""Callback function for EnOcean Device.
|
||||||
|
|
||||||
@ -112,7 +112,6 @@ class EnOceanDongle:
|
|||||||
device.value_changed(value)
|
device.value_changed(value)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
|
||||||
class EnOceanDevice():
|
class EnOceanDevice():
|
||||||
"""Parent class for all devices associated with the EnOcean component."""
|
"""Parent class for all devices associated with the EnOcean component."""
|
||||||
|
|
||||||
|
@ -77,8 +77,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument, too-many-function-args, too-many-locals
|
# pylint: disable=unused-argument
|
||||||
# pylint: disable=too-many-return-statements
|
|
||||||
def setup(hass, base_config):
|
def setup(hass, base_config):
|
||||||
"""Common setup for Envisalink devices."""
|
"""Common setup for Envisalink devices."""
|
||||||
from pyenvisalink import EnvisalinkAlarmPanel
|
from pyenvisalink import EnvisalinkAlarmPanel
|
||||||
|
@ -86,7 +86,6 @@ def is_on(hass, entity_id: str=None) -> bool:
|
|||||||
return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN]
|
return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN]
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def turn_on(hass, entity_id: str=None, speed: str=None) -> None:
|
def turn_on(hass, entity_id: str=None, speed: str=None) -> None:
|
||||||
"""Turn all or specified fan on."""
|
"""Turn all or specified fan on."""
|
||||||
data = {
|
data = {
|
||||||
@ -141,7 +140,6 @@ def set_speed(hass, entity_id: str=None, speed: str=None) -> None:
|
|||||||
hass.services.call(DOMAIN, SERVICE_SET_SPEED, data)
|
hass.services.call(DOMAIN, SERVICE_SET_SPEED, data)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches, too-many-locals, too-many-statements
|
|
||||||
def setup(hass, config: dict) -> None:
|
def setup(hass, config: dict) -> None:
|
||||||
"""Expose fan control via statemachine and services."""
|
"""Expose fan control via statemachine and services."""
|
||||||
component = EntityComponent(
|
component = EntityComponent(
|
||||||
@ -198,7 +196,7 @@ def setup(hass, config: dict) -> None:
|
|||||||
class FanEntity(ToggleEntity):
|
class FanEntity(ToggleEntity):
|
||||||
"""Representation of a fan."""
|
"""Representation of a fan."""
|
||||||
|
|
||||||
# pylint: disable=no-self-use, abstract-method
|
# pylint: disable=no-self-use
|
||||||
|
|
||||||
def set_speed(self: ToggleEntity, speed: str) -> None:
|
def set_speed(self: ToggleEntity, speed: str) -> None:
|
||||||
"""Set the speed of the fan."""
|
"""Set the speed of the fan."""
|
||||||
|
@ -110,11 +110,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
)])
|
)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
class MqttFan(FanEntity):
|
class MqttFan(FanEntity):
|
||||||
"""A MQTT fan component."""
|
"""A MQTT fan component."""
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
def __init__(self, hass, name, topic, templates, qos, retain, payload,
|
def __init__(self, hass, name, topic, templates, qos, retain, payload,
|
||||||
speed_list, optimistic):
|
speed_list, optimistic):
|
||||||
"""Initialize the MQTT fan."""
|
"""Initialize the MQTT fan."""
|
||||||
|
@ -44,7 +44,6 @@ def setup(hass, config):
|
|||||||
return len(feeds) > 0
|
return len(feeds) > 0
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
|
||||||
class FeedManager(object):
|
class FeedManager(object):
|
||||||
"""Abstraction over feedparser module."""
|
"""Abstraction over feedparser module."""
|
||||||
|
|
||||||
|
@ -4,14 +4,16 @@ Component that will help set the ffmpeg component.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/ffmpeg/
|
https://home-assistant.io/components/ffmpeg/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.util.async import run_coroutine_threadsafe
|
||||||
|
|
||||||
DOMAIN = 'ffmpeg'
|
DOMAIN = 'ffmpeg'
|
||||||
REQUIREMENTS = ["ha-ffmpeg==0.13"]
|
REQUIREMENTS = ["ha-ffmpeg==0.15"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -47,13 +49,26 @@ def setup(hass, config):
|
|||||||
|
|
||||||
|
|
||||||
def get_binary():
|
def get_binary():
|
||||||
"""Return ffmpeg binary from config."""
|
"""Return ffmpeg binary from config.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
return FFMPEG_CONFIG.get(CONF_FFMPEG_BIN)
|
return FFMPEG_CONFIG.get(CONF_FFMPEG_BIN)
|
||||||
|
|
||||||
|
|
||||||
def run_test(input_source):
|
def run_test(hass, input_source):
|
||||||
"""Run test on this input. TRUE is deactivate or run correct."""
|
"""Run test on this input. TRUE is deactivate or run correct."""
|
||||||
from haffmpeg import Test
|
return run_coroutine_threadsafe(
|
||||||
|
async_run_test(hass, input_source), hass.loop).result()
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_run_test(hass, input_source):
|
||||||
|
"""Run test on this input. TRUE is deactivate or run correct.
|
||||||
|
|
||||||
|
This method must be run in the event loop.
|
||||||
|
"""
|
||||||
|
from haffmpeg import TestAsync
|
||||||
|
|
||||||
if FFMPEG_CONFIG.get(CONF_RUN_TEST):
|
if FFMPEG_CONFIG.get(CONF_RUN_TEST):
|
||||||
# if in cache
|
# if in cache
|
||||||
@ -61,8 +76,9 @@ def run_test(input_source):
|
|||||||
return FFMPEG_TEST_CACHE[input_source]
|
return FFMPEG_TEST_CACHE[input_source]
|
||||||
|
|
||||||
# run test
|
# run test
|
||||||
test = Test(get_binary())
|
ffmpeg_test = TestAsync(get_binary(), loop=hass.loop)
|
||||||
if not test.run_test(input_source):
|
success = yield from ffmpeg_test.run_test(input_source)
|
||||||
|
if not success:
|
||||||
_LOGGER.error("FFmpeg '%s' test fails!", input_source)
|
_LOGGER.error("FFmpeg '%s' test fails!", input_source)
|
||||||
FFMPEG_TEST_CACHE[input_source] = False
|
FFMPEG_TEST_CACHE[input_source] = False
|
||||||
return False
|
return False
|
||||||
|
@ -4,14 +4,14 @@ Allows utilizing the Foursquare (Swarm) API.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/foursquare/
|
https://home-assistant.io/components/foursquare/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import json
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST
|
||||||
from homeassistant.config import load_yaml_config_file
|
from homeassistant.config import load_yaml_config_file
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
@ -75,7 +75,7 @@ def setup(hass, config):
|
|||||||
descriptions[DOMAIN][SERVICE_CHECKIN],
|
descriptions[DOMAIN][SERVICE_CHECKIN],
|
||||||
schema=CHECKIN_SERVICE_SCHEMA)
|
schema=CHECKIN_SERVICE_SCHEMA)
|
||||||
|
|
||||||
hass.wsgi.register_view(FoursquarePushReceiver(
|
hass.http.register_view(FoursquarePushReceiver(
|
||||||
hass, config[CONF_PUSH_SECRET]))
|
hass, config[CONF_PUSH_SECRET]))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -93,16 +93,21 @@ class FoursquarePushReceiver(HomeAssistantView):
|
|||||||
super().__init__(hass)
|
super().__init__(hass)
|
||||||
self.push_secret = push_secret
|
self.push_secret = push_secret
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Accept the POST from Foursquare."""
|
"""Accept the POST from Foursquare."""
|
||||||
raw_data = request.form
|
try:
|
||||||
_LOGGER.debug("Received Foursquare push: %s", raw_data)
|
data = yield from request.json()
|
||||||
if self.push_secret != raw_data["secret"]:
|
except ValueError:
|
||||||
|
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
|
secret = data.pop('secret', None)
|
||||||
|
|
||||||
|
_LOGGER.debug("Received Foursquare push: %s", data)
|
||||||
|
|
||||||
|
if self.push_secret != secret:
|
||||||
_LOGGER.error("Received Foursquare push with invalid"
|
_LOGGER.error("Received Foursquare push with invalid"
|
||||||
"push secret! Data: %s", raw_data)
|
"push secret: %s", secret)
|
||||||
return
|
return self.json_message('Incorrect secret', HTTP_BAD_REQUEST)
|
||||||
parsed_payload = {
|
|
||||||
key: json.loads(val) for key, val in raw_data.items()
|
self.hass.bus.async_fire(EVENT_PUSH, data)
|
||||||
if key != "secret"
|
|
||||||
}
|
|
||||||
self.hass.bus.fire(EVENT_PUSH, parsed_payload)
|
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
"""Handle the frontend for Home Assistant."""
|
"""Handle the frontend for Home Assistant."""
|
||||||
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
from aiohttp import web
|
||||||
from homeassistant.components import api
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_NOT_FOUND
|
||||||
|
from homeassistant.components import api, group
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from .version import FINGERPRINTS
|
from .version import FINGERPRINTS
|
||||||
|
|
||||||
@ -22,7 +27,6 @@ MANIFEST_JSON = {
|
|||||||
"icons": [],
|
"icons": [],
|
||||||
"lang": "en-US",
|
"lang": "en-US",
|
||||||
"name": "Home Assistant",
|
"name": "Home Assistant",
|
||||||
"orientation": "any",
|
|
||||||
"short_name": "Assistant",
|
"short_name": "Assistant",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"theme_color": "#03A9F4"
|
"theme_color": "#03A9F4"
|
||||||
@ -36,10 +40,9 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
def register_built_in_panel(hass, component_name, sidebar_title=None,
|
def register_built_in_panel(hass, component_name, sidebar_title=None,
|
||||||
sidebar_icon=None, url_path=None, config=None):
|
sidebar_icon=None, url_path=None, config=None):
|
||||||
"""Register a built-in panel."""
|
"""Register a built-in panel."""
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
path = 'panels/ha-panel-{}.html'.format(component_name)
|
path = 'panels/ha-panel-{}.html'.format(component_name)
|
||||||
|
|
||||||
if hass.wsgi.development:
|
if hass.http.development:
|
||||||
url = ('/static/home-assistant-polymer/panels/'
|
url = ('/static/home-assistant-polymer/panels/'
|
||||||
'{0}/ha-panel-{0}.html'.format(component_name))
|
'{0}/ha-panel-{0}.html'.format(component_name))
|
||||||
else:
|
else:
|
||||||
@ -65,7 +68,6 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
|||||||
|
|
||||||
Warning: this API will probably change. Use at own risk.
|
Warning: this API will probably change. Use at own risk.
|
||||||
"""
|
"""
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
if url_path is None:
|
if url_path is None:
|
||||||
url_path = component_name
|
url_path = component_name
|
||||||
|
|
||||||
@ -98,7 +100,7 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
|||||||
url = URL_PANEL_COMPONENT.format(component_name)
|
url = URL_PANEL_COMPONENT.format(component_name)
|
||||||
|
|
||||||
if url not in _REGISTERED_COMPONENTS:
|
if url not in _REGISTERED_COMPONENTS:
|
||||||
hass.wsgi.register_static_path(url, path)
|
hass.http.register_static_path(url, path)
|
||||||
_REGISTERED_COMPONENTS.add(url)
|
_REGISTERED_COMPONENTS.add(url)
|
||||||
|
|
||||||
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
|
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
|
||||||
@ -114,20 +116,23 @@ def add_manifest_json_key(key, val):
|
|||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup serving the frontend."""
|
"""Setup serving the frontend."""
|
||||||
hass.wsgi.register_view(BootstrapView)
|
hass.http.register_view(BootstrapView)
|
||||||
hass.wsgi.register_view(ManifestJSONView)
|
hass.http.register_view(ManifestJSONView)
|
||||||
|
|
||||||
if hass.wsgi.development:
|
if hass.http.development:
|
||||||
sw_path = "home-assistant-polymer/build/service_worker.js"
|
sw_path = "home-assistant-polymer/build/service_worker.js"
|
||||||
else:
|
else:
|
||||||
sw_path = "service_worker.js"
|
sw_path = "service_worker.js"
|
||||||
|
|
||||||
hass.wsgi.register_static_path("/service_worker.js",
|
hass.http.register_static_path("/service_worker.js",
|
||||||
os.path.join(STATIC_PATH, sw_path), 0)
|
os.path.join(STATIC_PATH, sw_path), 0)
|
||||||
hass.wsgi.register_static_path("/robots.txt",
|
hass.http.register_static_path("/robots.txt",
|
||||||
os.path.join(STATIC_PATH, "robots.txt"))
|
os.path.join(STATIC_PATH, "robots.txt"))
|
||||||
hass.wsgi.register_static_path("/static", STATIC_PATH)
|
hass.http.register_static_path("/static", STATIC_PATH)
|
||||||
hass.wsgi.register_static_path("/local", hass.config.path('www'))
|
|
||||||
|
local = hass.config.path('www')
|
||||||
|
if os.path.isdir(local):
|
||||||
|
hass.http.register_static_path("/local", local)
|
||||||
|
|
||||||
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
|
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
|
||||||
|
|
||||||
@ -140,7 +145,7 @@ def setup(hass, config):
|
|||||||
|
|
||||||
Done when Home Assistant is started so that all panels are known.
|
Done when Home Assistant is started so that all panels are known.
|
||||||
"""
|
"""
|
||||||
hass.wsgi.register_view(IndexView(
|
hass.http.register_view(IndexView(
|
||||||
hass, ['/{}'.format(name) for name in PANELS]))
|
hass, ['/{}'.format(name) for name in PANELS]))
|
||||||
|
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
|
||||||
@ -161,13 +166,14 @@ class BootstrapView(HomeAssistantView):
|
|||||||
url = "/api/bootstrap"
|
url = "/api/bootstrap"
|
||||||
name = "api:bootstrap"
|
name = "api:bootstrap"
|
||||||
|
|
||||||
|
@callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Return all data needed to bootstrap Home Assistant."""
|
"""Return all data needed to bootstrap Home Assistant."""
|
||||||
return self.json({
|
return self.json({
|
||||||
'config': self.hass.config.as_dict(),
|
'config': self.hass.config.as_dict(),
|
||||||
'states': self.hass.states.all(),
|
'states': self.hass.states.async_all(),
|
||||||
'events': api.events_json(self.hass),
|
'events': api.async_events_json(self.hass),
|
||||||
'services': api.services_json(self.hass),
|
'services': api.async_services_json(self.hass),
|
||||||
'panels': PANELS,
|
'panels': PANELS,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -178,7 +184,7 @@ class IndexView(HomeAssistantView):
|
|||||||
url = '/'
|
url = '/'
|
||||||
name = "frontend:index"
|
name = "frontend:index"
|
||||||
requires_auth = False
|
requires_auth = False
|
||||||
extra_urls = ['/states', '/states/<entity:entity_id>']
|
extra_urls = ['/states', '/states/{entity_id}']
|
||||||
|
|
||||||
def __init__(self, hass, extra_urls):
|
def __init__(self, hass, extra_urls):
|
||||||
"""Initialize the frontend view."""
|
"""Initialize the frontend view."""
|
||||||
@ -193,9 +199,17 @@ class IndexView(HomeAssistantView):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def get(self, request, entity_id=None):
|
def get(self, request, entity_id=None):
|
||||||
"""Serve the index view."""
|
"""Serve the index view."""
|
||||||
if self.hass.wsgi.development:
|
if entity_id is not None:
|
||||||
|
state = self.hass.states.get(entity_id)
|
||||||
|
|
||||||
|
if (not state or state.domain != 'group' or
|
||||||
|
not state.attributes.get(group.ATTR_VIEW)):
|
||||||
|
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||||
|
|
||||||
|
if self.hass.http.development:
|
||||||
core_url = '/static/home-assistant-polymer/build/core.js'
|
core_url = '/static/home-assistant-polymer/build/core.js'
|
||||||
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
|
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
|
||||||
else:
|
else:
|
||||||
@ -215,22 +229,24 @@ class IndexView(HomeAssistantView):
|
|||||||
if self.hass.config.api.api_password:
|
if self.hass.config.api.api_password:
|
||||||
# require password if set
|
# require password if set
|
||||||
no_auth = 'false'
|
no_auth = 'false'
|
||||||
if self.hass.wsgi.is_trusted_ip(
|
if self.hass.http.is_trusted_ip(
|
||||||
self.hass.wsgi.get_real_ip(request)):
|
self.hass.http.get_real_ip(request)):
|
||||||
# bypass for trusted networks
|
# bypass for trusted networks
|
||||||
no_auth = 'true'
|
no_auth = 'true'
|
||||||
|
|
||||||
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
|
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
|
||||||
template = self.templates.get_template('index.html')
|
template = yield from self.hass.loop.run_in_executor(
|
||||||
|
None, self.templates.get_template, 'index.html')
|
||||||
|
|
||||||
# pylint is wrong
|
# pylint is wrong
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
|
# This is a jinja2 template, not a HA template so we call 'render'.
|
||||||
resp = template.render(
|
resp = template.render(
|
||||||
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
|
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
|
||||||
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
|
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
|
||||||
panel_url=panel_url, panels=PANELS)
|
panel_url=panel_url, panels=PANELS)
|
||||||
|
|
||||||
return self.Response(resp, mimetype='text/html')
|
return web.Response(text=resp, content_type='text/html')
|
||||||
|
|
||||||
|
|
||||||
class ManifestJSONView(HomeAssistantView):
|
class ManifestJSONView(HomeAssistantView):
|
||||||
@ -240,8 +256,8 @@ class ManifestJSONView(HomeAssistantView):
|
|||||||
url = "/manifest.json"
|
url = "/manifest.json"
|
||||||
name = "manifestjson"
|
name = "manifestjson"
|
||||||
|
|
||||||
def get(self, request):
|
@asyncio.coroutine
|
||||||
|
def get(self, request): # pylint: disable=no-self-use
|
||||||
"""Return the manifest.json."""
|
"""Return the manifest.json."""
|
||||||
import json
|
|
||||||
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8')
|
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8')
|
||||||
return self.Response(msg, mimetype="application/manifest+json")
|
return web.Response(body=msg, content_type="application/manifest+json")
|
||||||
|
@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
FINGERPRINTS = {
|
FINGERPRINTS = {
|
||||||
"core.js": "5ed5e063d66eb252b5b288738c9c2d16",
|
"core.js": "5ed5e063d66eb252b5b288738c9c2d16",
|
||||||
"frontend.html": "0a4c2c6e86a0a78c2ff3e03842de609d",
|
"frontend.html": "78be2dfedc4e95326cbcd9401fb17b4d",
|
||||||
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
||||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||||
"panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002",
|
"panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002",
|
||||||
"panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a",
|
"panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a",
|
||||||
"panels/ha-panel-dev-service.html": "d33657c964041d3ebf114e90a922a15e",
|
"panels/ha-panel-dev-service.html": "4a051878b92b002b8b018774ba207769",
|
||||||
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
|
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
|
||||||
"panels/ha-panel-dev-template.html": "d23943fa0370f168714da407c90091a2",
|
"panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400",
|
||||||
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
||||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||||
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
|
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
|
||||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1 +1 @@
|
|||||||
Subproject commit f3081ed48fd11fa89586701dba3792d028473a15
|
Subproject commit 896e0427675bb99348de6f1453bd6f8cf48b5c6f
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user