commit
cd8723f742
|
@ -152,6 +152,7 @@ omit =
|
||||||
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
|
||||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||||
|
homeassistant/components/alarm_control_panel/totalconnect.py
|
||||||
homeassistant/components/apiai.py
|
homeassistant/components/apiai.py
|
||||||
homeassistant/components/binary_sensor/arest.py
|
homeassistant/components/binary_sensor/arest.py
|
||||||
homeassistant/components/binary_sensor/concord232.py
|
homeassistant/components/binary_sensor/concord232.py
|
||||||
|
@ -225,6 +226,7 @@ omit =
|
||||||
homeassistant/components/light/hue.py
|
homeassistant/components/light/hue.py
|
||||||
homeassistant/components/light/hyperion.py
|
homeassistant/components/light/hyperion.py
|
||||||
homeassistant/components/light/lifx.py
|
homeassistant/components/light/lifx.py
|
||||||
|
homeassistant/components/light/lifx_legacy.py
|
||||||
homeassistant/components/light/limitlessled.py
|
homeassistant/components/light/limitlessled.py
|
||||||
homeassistant/components/light/osramlightify.py
|
homeassistant/components/light/osramlightify.py
|
||||||
homeassistant/components/light/tikteck.py
|
homeassistant/components/light/tikteck.py
|
||||||
|
@ -235,6 +237,7 @@ omit =
|
||||||
homeassistant/components/light/zengge.py
|
homeassistant/components/light/zengge.py
|
||||||
homeassistant/components/lirc.py
|
homeassistant/components/lirc.py
|
||||||
homeassistant/components/lock/nuki.py
|
homeassistant/components/lock/nuki.py
|
||||||
|
homeassistant/components/lock/lockitron.py
|
||||||
homeassistant/components/media_player/anthemav.py
|
homeassistant/components/media_player/anthemav.py
|
||||||
homeassistant/components/media_player/apple_tv.py
|
homeassistant/components/media_player/apple_tv.py
|
||||||
homeassistant/components/media_player/aquostv.py
|
homeassistant/components/media_player/aquostv.py
|
||||||
|
@ -323,6 +326,7 @@ omit =
|
||||||
homeassistant/components/sensor/coinmarketcap.py
|
homeassistant/components/sensor/coinmarketcap.py
|
||||||
homeassistant/components/sensor/comed_hourly_pricing.py
|
homeassistant/components/sensor/comed_hourly_pricing.py
|
||||||
homeassistant/components/sensor/cpuspeed.py
|
homeassistant/components/sensor/cpuspeed.py
|
||||||
|
homeassistant/components/sensor/crimereports.py
|
||||||
homeassistant/components/sensor/cups.py
|
homeassistant/components/sensor/cups.py
|
||||||
homeassistant/components/sensor/currencylayer.py
|
homeassistant/components/sensor/currencylayer.py
|
||||||
homeassistant/components/sensor/darksky.py
|
homeassistant/components/sensor/darksky.py
|
||||||
|
@ -332,6 +336,7 @@ omit =
|
||||||
homeassistant/components/sensor/dovado.py
|
homeassistant/components/sensor/dovado.py
|
||||||
homeassistant/components/sensor/dte_energy_bridge.py
|
homeassistant/components/sensor/dte_energy_bridge.py
|
||||||
homeassistant/components/sensor/ebox.py
|
homeassistant/components/sensor/ebox.py
|
||||||
|
homeassistant/components/sensor/eddystone_temperature.py
|
||||||
homeassistant/components/sensor/eliqonline.py
|
homeassistant/components/sensor/eliqonline.py
|
||||||
homeassistant/components/sensor/emoncms.py
|
homeassistant/components/sensor/emoncms.py
|
||||||
homeassistant/components/sensor/fastdotcom.py
|
homeassistant/components/sensor/fastdotcom.py
|
||||||
|
@ -357,9 +362,11 @@ omit =
|
||||||
homeassistant/components/sensor/linux_battery.py
|
homeassistant/components/sensor/linux_battery.py
|
||||||
homeassistant/components/sensor/loopenergy.py
|
homeassistant/components/sensor/loopenergy.py
|
||||||
homeassistant/components/sensor/lyft.py
|
homeassistant/components/sensor/lyft.py
|
||||||
|
homeassistant/components/sensor/metoffice.py
|
||||||
homeassistant/components/sensor/miflora.py
|
homeassistant/components/sensor/miflora.py
|
||||||
homeassistant/components/sensor/modem_callerid.py
|
homeassistant/components/sensor/modem_callerid.py
|
||||||
homeassistant/components/sensor/mqtt_room.py
|
homeassistant/components/sensor/mqtt_room.py
|
||||||
|
homeassistant/components/sensor/mvglive.py
|
||||||
homeassistant/components/sensor/netdata.py
|
homeassistant/components/sensor/netdata.py
|
||||||
homeassistant/components/sensor/neurio_energy.py
|
homeassistant/components/sensor/neurio_energy.py
|
||||||
homeassistant/components/sensor/nut.py
|
homeassistant/components/sensor/nut.py
|
||||||
|
@ -431,12 +438,12 @@ omit =
|
||||||
homeassistant/components/tts/picotts.py
|
homeassistant/components/tts/picotts.py
|
||||||
homeassistant/components/upnp.py
|
homeassistant/components/upnp.py
|
||||||
homeassistant/components/weather/bom.py
|
homeassistant/components/weather/bom.py
|
||||||
|
homeassistant/components/weather/metoffice.py
|
||||||
homeassistant/components/weather/openweathermap.py
|
homeassistant/components/weather/openweathermap.py
|
||||||
homeassistant/components/weather/zamg.py
|
homeassistant/components/weather/zamg.py
|
||||||
homeassistant/components/zeroconf.py
|
homeassistant/components/zeroconf.py
|
||||||
homeassistant/components/zwave/__init__.py
|
homeassistant/components/zwave/__init__.py
|
||||||
homeassistant/components/zwave/util.py
|
homeassistant/components/zwave/util.py
|
||||||
homeassistant/components/zwave/workaround.py
|
|
||||||
|
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
|
|
|
@ -1,15 +1,4 @@
|
||||||
config/*
|
config/*
|
||||||
!config/home-assistant.conf.default
|
|
||||||
|
|
||||||
# There is not a better solution afaik..
|
|
||||||
!config/custom_components
|
|
||||||
config/custom_components/*
|
|
||||||
!config/custom_components/example.py
|
|
||||||
!config/custom_components/hello_world.py
|
|
||||||
!config/custom_components/mqtt_example.py
|
|
||||||
!config/panels
|
|
||||||
config/panels/*
|
|
||||||
!config/panels/react.html
|
|
||||||
|
|
||||||
tests/testing_config/deps
|
tests/testing_config/deps
|
||||||
tests/testing_config/home-assistant.log
|
tests/testing_config/home-assistant.log
|
||||||
|
|
83
README.rst
83
README.rst
|
@ -1,9 +1,7 @@
|
||||||
Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/home-assistant/home-assistant| |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs|
|
Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/home-assistant/home-assistant| |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs|
|
||||||
==============================================================================================================================================================================================
|
==============================================================================================================================================================================================
|
||||||
|
|
||||||
Home Assistant is a home automation platform running on Python 3. The
|
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
|
||||||
goal of Home Assistant is to be able to track and control all devices at
|
|
||||||
home and offer a platform for automating control.
|
|
||||||
|
|
||||||
To get started:
|
To get started:
|
||||||
|
|
||||||
|
@ -12,83 +10,22 @@ To get started:
|
||||||
python3 -m pip install homeassistant
|
python3 -m pip install homeassistant
|
||||||
hass --open-ui
|
hass --open-ui
|
||||||
|
|
||||||
Check out `the website <https://home-assistant.io>`__ for `a
|
Check out `home-assistant.io <https://home-assistant.io>`__ for `a
|
||||||
demo <https://home-assistant.io/demo/>`__, installation instructions,
|
demo <https://home-assistant.io/demo/>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
|
||||||
tutorials and documentation.
|
`tutorials <https://home-assistant.io/getting-started/automation-2/>`__ and `documentation <https://home-assistant.io/docs/>`__.
|
||||||
|
|
||||||
|screenshot-states|
|
|screenshot-states|
|
||||||
|
|
||||||
Examples of devices Home Assistant can interface with:
|
Featured integrations
|
||||||
|
---------------------
|
||||||
|
|
||||||
- Monitoring connected devices to a wireless router:
|
|screenshot-components|
|
||||||
`OpenWrt <https://openwrt.org/>`__,
|
|
||||||
`Tomato <http://www.polarcloud.com/tomato>`__,
|
|
||||||
`Netgear <http://netgear.com>`__,
|
|
||||||
`DD-WRT <http://www.dd-wrt.com/site/index>`__,
|
|
||||||
`TPLink <http://www.tp-link.us/>`__,
|
|
||||||
`ASUSWRT <http://event.asus.com/2013/nw/ASUSWRT/>`__,
|
|
||||||
`Xiaomi <http://miwifi.com/>`__ and any SNMP
|
|
||||||
capable Linksys WAP/WRT
|
|
||||||
- `Philips Hue <http://meethue.com>`__ lights,
|
|
||||||
`WeMo <http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/>`__
|
|
||||||
switches, `Edimax <http://www.edimax.com/>`__ switches,
|
|
||||||
`Efergy <https://efergy.com>`__ energy monitoring, and
|
|
||||||
`Tellstick <http://www.telldus.se/products/tellstick>`__ devices and
|
|
||||||
sensors
|
|
||||||
- `Google
|
|
||||||
Chromecasts <http://www.google.com/intl/en/chrome/devices/chromecast>`__,
|
|
||||||
`Music Player Daemon <http://www.musicpd.org/>`__, `Logitech
|
|
||||||
Squeezebox <https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29>`__,
|
|
||||||
`Plex <https://plex.tv/>`__, `Kodi (XBMC) <http://kodi.tv/>`__,
|
|
||||||
iTunes (by way of
|
|
||||||
`itunes-api <https://github.com/maddox/itunes-api>`__), and Amazon
|
|
||||||
Fire TV (by way of
|
|
||||||
`python-firetv <https://github.com/happyleavesaoc/python-firetv>`__)
|
|
||||||
- Support for
|
|
||||||
`ISY994 <https://www.universal-devices.com/residential/isy994i-series/>`__
|
|
||||||
(Insteon and X10 devices), `Z-Wave <http://www.z-wave.com/>`__, `Nest
|
|
||||||
Thermostats <https://nest.com/>`__,
|
|
||||||
`RFXtrx <http://www.rfxcom.com/>`__,
|
|
||||||
`Arduino <https://www.arduino.cc/>`__, `Raspberry
|
|
||||||
Pi <https://www.raspberrypi.org/>`__, and
|
|
||||||
`Modbus <http://www.modbus.org/>`__
|
|
||||||
- Interaction with `IFTTT <https://ifttt.com/>`__
|
|
||||||
- Integrate data from the `Bitcoin <https://bitcoin.org>`__ network,
|
|
||||||
meteorological data from
|
|
||||||
`OpenWeatherMap <http://openweathermap.org/>`__ and
|
|
||||||
`Forecast.io <https://forecast.io/>`__,
|
|
||||||
`Transmission <http://www.transmissionbt.com/>`__, or
|
|
||||||
`SABnzbd <http://sabnzbd.org>`__.
|
|
||||||
- `See full list of supported
|
|
||||||
devices <https://home-assistant.io/components/>`__
|
|
||||||
|
|
||||||
Build home automation on top of your devices:
|
The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture <https://home-assistant.io/developers/architecture/>`__ and the `section on creating your own
|
||||||
|
|
||||||
- Keep a precise history of every change to the state of your house
|
|
||||||
- Turn on the lights when people get home after sunset
|
|
||||||
- Turn on lights slowly during sunset to compensate for less light
|
|
||||||
- Turn off all lights and devices when everybody leaves the house
|
|
||||||
- Offers a `REST API <https://home-assistant.io/developers/rest_api/>`__
|
|
||||||
and can interface with MQTT for easy integration with other projects
|
|
||||||
like `OwnTracks <http://owntracks.org/>`__
|
|
||||||
- Allow sending notifications using
|
|
||||||
`Instapush <https://instapush.im>`__, `Notify My Android
|
|
||||||
(NMA) <http://www.notifymyandroid.com/>`__,
|
|
||||||
`PushBullet <https://www.pushbullet.com/>`__,
|
|
||||||
`PushOver <https://pushover.net/>`__,
|
|
||||||
`Slack <https://slack.com/>`__,
|
|
||||||
`Telegram <https://telegram.org/>`__, `Join <http://joaoapps.com/join/>`__, and `Jabber
|
|
||||||
(XMPP) <http://xmpp.org>`__
|
|
||||||
|
|
||||||
The system is built using a modular approach so support for other devices or actions can
|
|
||||||
be implemented easily. See also the `section on
|
|
||||||
architecture <https://home-assistant.io/developers/architecture/>`__
|
|
||||||
and the `section on creating your own
|
|
||||||
components <https://home-assistant.io/developers/creating_components/>`__.
|
components <https://home-assistant.io/developers/creating_components/>`__.
|
||||||
|
|
||||||
If you run into issues while using Home Assistant or during development
|
If you run into issues while using Home Assistant or during development
|
||||||
of a component, check the `Home Assistant help
|
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
||||||
section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
|
||||||
|
|
||||||
.. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=master
|
.. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=master
|
||||||
:target: https://travis-ci.org/home-assistant/home-assistant
|
:target: https://travis-ci.org/home-assistant/home-assistant
|
||||||
|
@ -100,3 +37,5 @@ section <https://home-assistant.io/help/>`__ of our website for further help and
|
||||||
:target: https://gitter.im/home-assistant/home-assistant/devs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
:target: https://gitter.im/home-assistant/home-assistant/devs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||||
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
|
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
|
||||||
:target: https://home-assistant.io/demo/
|
:target: https://home-assistant.io/demo/
|
||||||
|
.. |screenshot-components| image:: https://raw.github.com/home-assistant/home-assistant/dev/docs/screenshot-components.png
|
||||||
|
:target: https://home-assistant.io/components/
|
||||||
|
|
|
@ -1,158 +0,0 @@
|
||||||
homeassistant:
|
|
||||||
# Omitted values in this section will be auto detected using freegeoip.io
|
|
||||||
|
|
||||||
# Location required to calculate the time the sun rises and sets.
|
|
||||||
# Coordinates are also used for location for weather related components.
|
|
||||||
# Google Maps can be used to determine more precise GPS coordinates.
|
|
||||||
latitude: 32.87336
|
|
||||||
longitude: 117.22743
|
|
||||||
|
|
||||||
# Impacts weather/sunrise data
|
|
||||||
elevation: 665
|
|
||||||
|
|
||||||
# 'metric' for Metric System, 'imperial' for imperial system
|
|
||||||
unit_system: metric
|
|
||||||
|
|
||||||
# Pick yours from here:
|
|
||||||
# http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
|
||||||
time_zone: America/Los_Angeles
|
|
||||||
|
|
||||||
# Name of the location where Home Assistant is running
|
|
||||||
name: Home
|
|
||||||
|
|
||||||
http:
|
|
||||||
api_password: mypass
|
|
||||||
# Set to 1 to enable development mode
|
|
||||||
# development: 1
|
|
||||||
|
|
||||||
# Enable the frontend
|
|
||||||
frontend:
|
|
||||||
|
|
||||||
light:
|
|
||||||
# platform: hue
|
|
||||||
|
|
||||||
wink:
|
|
||||||
# Get your token at https://winkbearertoken.appspot.com
|
|
||||||
access_token: 'YOUR_TOKEN'
|
|
||||||
|
|
||||||
device_tracker:
|
|
||||||
# The following tracker are available:
|
|
||||||
# https://home-assistant.io/components/#presence-detection
|
|
||||||
platform: netgear
|
|
||||||
host: 192.168.1.1
|
|
||||||
username: admin
|
|
||||||
password: PASSWORD
|
|
||||||
|
|
||||||
switch:
|
|
||||||
platform: wemo
|
|
||||||
|
|
||||||
climate:
|
|
||||||
platform: nest
|
|
||||||
# Required: username and password that are used to login to the Nest thermostat.
|
|
||||||
username: myemail@mydomain.com
|
|
||||||
password: mypassword
|
|
||||||
|
|
||||||
downloader:
|
|
||||||
download_dir: downloads
|
|
||||||
|
|
||||||
notify:
|
|
||||||
platform: pushbullet
|
|
||||||
api_key: ABCDEFGHJKLMNOPQRSTUVXYZ
|
|
||||||
|
|
||||||
device_sun_light_trigger:
|
|
||||||
# Optional: specify a specific light/group of lights that has to be turned on
|
|
||||||
light_group: group.living_room
|
|
||||||
# Optional: specify which light profile to use when turning lights on
|
|
||||||
light_profile: relax
|
|
||||||
# Optional: disable lights being turned off when everybody leaves the house
|
|
||||||
# disable_turn_off: 1
|
|
||||||
|
|
||||||
# A comma separated list of states that have to be tracked as a single group
|
|
||||||
# Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME)
|
|
||||||
# You can also have groups within groups.
|
|
||||||
# https://home-assistant.io/components/group/
|
|
||||||
group:
|
|
||||||
default_view:
|
|
||||||
view: yes
|
|
||||||
entities:
|
|
||||||
- group.awesome_people
|
|
||||||
- group.climate
|
|
||||||
kitchen:
|
|
||||||
name: Kitchen
|
|
||||||
entities:
|
|
||||||
- switch.kitchen_pin_3
|
|
||||||
upstairs:
|
|
||||||
name: Kids
|
|
||||||
icon: mdi:account-multiple
|
|
||||||
view: yes
|
|
||||||
entities:
|
|
||||||
- input_boolean.notify_home
|
|
||||||
- camera.demo_camera
|
|
||||||
|
|
||||||
browser:
|
|
||||||
keyboard:
|
|
||||||
|
|
||||||
# https://home-assistant.io/getting-started/automation/
|
|
||||||
automation:
|
|
||||||
- alias: Turn on light when sun sets
|
|
||||||
trigger:
|
|
||||||
platform: sun
|
|
||||||
event: sunset
|
|
||||||
offset: "-01:00:00"
|
|
||||||
condition:
|
|
||||||
condition: state
|
|
||||||
entity_id: group.all_devices
|
|
||||||
state: 'home'
|
|
||||||
action:
|
|
||||||
service: light.turn_on
|
|
||||||
|
|
||||||
# Another way to do is to collect all entries under one "sensor:"
|
|
||||||
# sensor:
|
|
||||||
# - platform: mqtt
|
|
||||||
# name: "MQTT Sensor 1"
|
|
||||||
# - platform: mqtt
|
|
||||||
# name: "MQTT Sensor 2"
|
|
||||||
#
|
|
||||||
# Details: https://home-assistant.io/getting-started/devices/
|
|
||||||
|
|
||||||
sensor:
|
|
||||||
platform: systemmonitor
|
|
||||||
resources:
|
|
||||||
- type: 'disk_use_percent'
|
|
||||||
arg: '/'
|
|
||||||
- type: 'disk_use_percent'
|
|
||||||
arg: '/home'
|
|
||||||
|
|
||||||
sensor 2:
|
|
||||||
platform: cpuspeed
|
|
||||||
|
|
||||||
script:
|
|
||||||
wakeup:
|
|
||||||
alias: Wake Up
|
|
||||||
sequence:
|
|
||||||
- event: LOGBOOK_ENTRY
|
|
||||||
event_data:
|
|
||||||
name: Paulus
|
|
||||||
message: is waking up
|
|
||||||
entity_id: device_tracker.paulus
|
|
||||||
domain: light
|
|
||||||
- alias: Bedroom lights on
|
|
||||||
service: light.turn_on
|
|
||||||
data:
|
|
||||||
entity_id: group.bedroom
|
|
||||||
brightness: 100
|
|
||||||
- delay:
|
|
||||||
minutes: 1
|
|
||||||
- alias: Living room lights on
|
|
||||||
service: light.turn_on
|
|
||||||
data:
|
|
||||||
entity_id: group.living_room
|
|
||||||
|
|
||||||
scene:
|
|
||||||
- name: Romantic
|
|
||||||
entities:
|
|
||||||
light.tv_back_light: on
|
|
||||||
light.ceiling:
|
|
||||||
state: on
|
|
||||||
xy_color: [0.33, 0.66]
|
|
||||||
brightness: 200
|
|
|
@ -1,149 +0,0 @@
|
||||||
"""
|
|
||||||
Example of a custom component.
|
|
||||||
|
|
||||||
Example component to target an entity_id to:
|
|
||||||
- turn it on at 7AM in the morning
|
|
||||||
- turn it on if anyone comes home and it is off
|
|
||||||
- turn it off if all lights are turned off
|
|
||||||
- turn it off if all people leave the house
|
|
||||||
- offer a service to turn it on for 10 seconds
|
|
||||||
|
|
||||||
Configuration:
|
|
||||||
|
|
||||||
To use the Example custom component you will need to add the following to
|
|
||||||
your configuration.yaml file.
|
|
||||||
|
|
||||||
example:
|
|
||||||
target: TARGET_ENTITY
|
|
||||||
|
|
||||||
Variable:
|
|
||||||
|
|
||||||
target
|
|
||||||
*Required
|
|
||||||
TARGET_ENTITY should be one of your devices that can be turned on and off,
|
|
||||||
ie a light or a switch. Example value could be light.Ceiling or switch.AC
|
|
||||||
(if you have these devices with those names).
|
|
||||||
"""
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF
|
|
||||||
from homeassistant.helpers import validate_config
|
|
||||||
from homeassistant.helpers.event_decorators import \
|
|
||||||
track_state_change, track_time_change
|
|
||||||
from homeassistant.helpers.service import service
|
|
||||||
import homeassistant.components as core
|
|
||||||
from homeassistant.components import device_tracker
|
|
||||||
from homeassistant.components import light
|
|
||||||
|
|
||||||
# The domain of your component. Should be equal to the name of your component.
|
|
||||||
DOMAIN = "example"
|
|
||||||
|
|
||||||
# List of component names (string) your component depends upon.
|
|
||||||
# We depend on group because group will be loaded after all the components that
|
|
||||||
# initialize devices have been setup.
|
|
||||||
DEPENDENCIES = ['group', 'device_tracker', 'light']
|
|
||||||
|
|
||||||
# Configuration key for the entity id we are targeting.
|
|
||||||
CONF_TARGET = 'target'
|
|
||||||
|
|
||||||
# Variable for storing configuration parameters.
|
|
||||||
TARGET_ID = None
|
|
||||||
|
|
||||||
# Name of the service that we expose.
|
|
||||||
SERVICE_FLASH = 'flash'
|
|
||||||
|
|
||||||
# Shortcut for the logger
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
|
||||||
"""Setup example component."""
|
|
||||||
global TARGET_ID
|
|
||||||
|
|
||||||
# Validate that all required config options are given.
|
|
||||||
if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER):
|
|
||||||
return False
|
|
||||||
|
|
||||||
TARGET_ID = config[DOMAIN][CONF_TARGET]
|
|
||||||
|
|
||||||
# Validate that the target entity id exists.
|
|
||||||
if hass.states.get(TARGET_ID) is None:
|
|
||||||
_LOGGER.error("Target entity id %s does not exist",
|
|
||||||
TARGET_ID)
|
|
||||||
|
|
||||||
# Tell the bootstrapper that we failed to initialize and clear the
|
|
||||||
# stored target id so our functions don't run.
|
|
||||||
TARGET_ID = None
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Tell the bootstrapper that we initialized successfully.
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES)
|
|
||||||
def track_devices(hass, entity_id, old_state, new_state):
|
|
||||||
"""Called when the group.all devices change state."""
|
|
||||||
# If the target id is not set, return
|
|
||||||
if not TARGET_ID:
|
|
||||||
return
|
|
||||||
|
|
||||||
# If anyone comes home and the entity is not on, turn it on.
|
|
||||||
if new_state.state == STATE_HOME and not core.is_on(hass, TARGET_ID):
|
|
||||||
|
|
||||||
core.turn_on(hass, TARGET_ID)
|
|
||||||
|
|
||||||
# If all people leave the house and the entity is on, turn it off.
|
|
||||||
elif new_state.state == STATE_NOT_HOME and core.is_on(hass, TARGET_ID):
|
|
||||||
|
|
||||||
core.turn_off(hass, TARGET_ID)
|
|
||||||
|
|
||||||
|
|
||||||
@track_time_change(hour=7, minute=0, second=0)
|
|
||||||
def wake_up(hass, now):
|
|
||||||
"""Turn light on in the morning.
|
|
||||||
|
|
||||||
Turn the light on at 7 AM if there are people home and it is not already
|
|
||||||
on.
|
|
||||||
"""
|
|
||||||
if not TARGET_ID:
|
|
||||||
return
|
|
||||||
|
|
||||||
if device_tracker.is_on(hass) and not core.is_on(hass, TARGET_ID):
|
|
||||||
_LOGGER.info('People home at 7AM, turning it on')
|
|
||||||
core.turn_on(hass, TARGET_ID)
|
|
||||||
|
|
||||||
|
|
||||||
@track_state_change(light.ENTITY_ID_ALL_LIGHTS, STATE_ON, STATE_OFF)
|
|
||||||
def all_lights_off(hass, entity_id, old_state, new_state):
|
|
||||||
"""If all lights turn off, turn off."""
|
|
||||||
if not TARGET_ID:
|
|
||||||
return
|
|
||||||
|
|
||||||
if core.is_on(hass, TARGET_ID):
|
|
||||||
_LOGGER.info('All lights have been turned off, turning it off')
|
|
||||||
core.turn_off(hass, TARGET_ID)
|
|
||||||
|
|
||||||
|
|
||||||
@service(DOMAIN, SERVICE_FLASH)
|
|
||||||
def flash_service(hass, call):
|
|
||||||
"""Service that will toggle the target.
|
|
||||||
|
|
||||||
Set the light to off for 10 seconds if on and vice versa.
|
|
||||||
"""
|
|
||||||
if not TARGET_ID:
|
|
||||||
return
|
|
||||||
|
|
||||||
if core.is_on(hass, TARGET_ID):
|
|
||||||
core.turn_off(hass, TARGET_ID)
|
|
||||||
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
core.turn_on(hass, TARGET_ID)
|
|
||||||
|
|
||||||
else:
|
|
||||||
core.turn_on(hass, TARGET_ID)
|
|
||||||
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
core.turn_off(hass, TARGET_ID)
|
|
|
@ -1,27 +0,0 @@
|
||||||
"""
|
|
||||||
The "hello world" custom component.
|
|
||||||
|
|
||||||
This component implements the bare minimum that a component should implement.
|
|
||||||
|
|
||||||
Configuration:
|
|
||||||
|
|
||||||
To use the hello_word component you will need to add the following to your
|
|
||||||
configuration.yaml file.
|
|
||||||
|
|
||||||
hello_world:
|
|
||||||
"""
|
|
||||||
|
|
||||||
# The domain of your component. Should be equal to the name of your component.
|
|
||||||
DOMAIN = "hello_world"
|
|
||||||
|
|
||||||
# List of component names (string) your component depends upon.
|
|
||||||
DEPENDENCIES = []
|
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
|
||||||
"""Setup our skeleton component."""
|
|
||||||
# States are in the format DOMAIN.OBJECT_ID.
|
|
||||||
hass.states.set('hello_world.Hello_World', 'Works!')
|
|
||||||
|
|
||||||
# Return boolean to indicate that initialization was successfully.
|
|
||||||
return True
|
|
|
@ -1,55 +0,0 @@
|
||||||
"""
|
|
||||||
Example of a custom MQTT component.
|
|
||||||
|
|
||||||
Shows how to communicate with MQTT. Follows a topic on MQTT and updates the
|
|
||||||
state of an entity to the last message received on that topic.
|
|
||||||
|
|
||||||
Also offers a service 'set_state' that will publish a message on the topic that
|
|
||||||
will be passed via MQTT to our message received listener. Call the service with
|
|
||||||
example payload {"new_state": "some new state"}.
|
|
||||||
|
|
||||||
Configuration:
|
|
||||||
|
|
||||||
To use the mqtt_example component you will need to add the following to your
|
|
||||||
configuration.yaml file.
|
|
||||||
|
|
||||||
mqtt_example:
|
|
||||||
topic: "home-assistant/mqtt_example"
|
|
||||||
"""
|
|
||||||
import homeassistant.loader as loader
|
|
||||||
|
|
||||||
# The domain of your component. Should be equal to the name of your component.
|
|
||||||
DOMAIN = "mqtt_example"
|
|
||||||
|
|
||||||
# List of component names (string) your component depends upon.
|
|
||||||
DEPENDENCIES = ['mqtt']
|
|
||||||
|
|
||||||
CONF_TOPIC = 'topic'
|
|
||||||
DEFAULT_TOPIC = 'home-assistant/mqtt_example'
|
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
|
||||||
"""Setup the MQTT example component."""
|
|
||||||
mqtt = loader.get_component('mqtt')
|
|
||||||
topic = config[DOMAIN].get('topic', DEFAULT_TOPIC)
|
|
||||||
entity_id = 'mqtt_example.last_message'
|
|
||||||
|
|
||||||
# Listen to a message on MQTT.
|
|
||||||
def message_received(topic, payload, qos):
|
|
||||||
"""A new MQTT message has been received."""
|
|
||||||
hass.states.set(entity_id, payload)
|
|
||||||
|
|
||||||
mqtt.subscribe(hass, topic, message_received)
|
|
||||||
|
|
||||||
hass.states.set(entity_id, 'No messages')
|
|
||||||
|
|
||||||
# Service to publish a message on MQTT.
|
|
||||||
def set_state_service(call):
|
|
||||||
"""Service to send a message."""
|
|
||||||
mqtt.publish(hass, topic, call.data.get('new_state'))
|
|
||||||
|
|
||||||
# Register our service with Home Assistant.
|
|
||||||
hass.services.register(DOMAIN, 'set_state', set_state_service)
|
|
||||||
|
|
||||||
# Return boolean to indicate that initialization was successfully.
|
|
||||||
return True
|
|
|
@ -1,432 +0,0 @@
|
||||||
<!--
|
|
||||||
Custom Home Assistant panel example.
|
|
||||||
|
|
||||||
Currently only works in Firefox and Chrome because it uses ES6.
|
|
||||||
|
|
||||||
Make sure this file is in <config>/panels/react.html
|
|
||||||
|
|
||||||
Add to your configuration.yaml:
|
|
||||||
|
|
||||||
panel_custom:
|
|
||||||
- name: react
|
|
||||||
sidebar_title: TodoMVC
|
|
||||||
sidebar_icon: mdi:checkbox-marked-outline
|
|
||||||
config:
|
|
||||||
title: Wow hello!
|
|
||||||
-->
|
|
||||||
|
|
||||||
<script src="https://fb.me/react-15.2.1.min.js"></script>
|
|
||||||
<script src="https://fb.me/react-dom-15.2.1.min.js"></script>
|
|
||||||
|
|
||||||
<!-- for development, replace with:
|
|
||||||
<script src="https://fb.me/react-15.2.1.js"></script>
|
|
||||||
<script src="https://fb.me/react-dom-15.2.1.js"></script>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!--
|
|
||||||
CSS taken from ReactJS TodoMVC example by Pete Hunt
|
|
||||||
http://todomvc.com/examples/react/
|
|
||||||
-->
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.todoapp input[type="checkbox"] {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp {
|
|
||||||
background: #fff;
|
|
||||||
margin: 130px 0 40px 0;
|
|
||||||
position: relative;
|
|
||||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
|
|
||||||
0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp h1 {
|
|
||||||
position: absolute;
|
|
||||||
top: -155px;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 100px;
|
|
||||||
font-weight: 100;
|
|
||||||
text-align: center;
|
|
||||||
color: rgba(175, 47, 47, 0.15);
|
|
||||||
-webkit-text-rendering: optimizeLegibility;
|
|
||||||
-moz-text-rendering: optimizeLegibility;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .main {
|
|
||||||
position: relative;
|
|
||||||
border-top: 1px solid #e6e6e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .todo-list {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .todo-list li {
|
|
||||||
position: relative;
|
|
||||||
font-size: 24px;
|
|
||||||
border-bottom: 1px solid #ededed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .todo-list li:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .todo-list li .toggle {
|
|
||||||
text-align: center;
|
|
||||||
width: 40px;
|
|
||||||
/* auto, since non-WebKit browsers doesn't support input styling */
|
|
||||||
height: auto;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: auto 0;
|
|
||||||
border: none; /* Mobile Safari */
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .todo-list li .toggle:focus {
|
|
||||||
border-left: 3px solid rgba(175, 47, 47, 0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .todo-list li .toggle:after {
|
|
||||||
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .todo-list li .toggle:checked:after {
|
|
||||||
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .todo-list li label {
|
|
||||||
white-space: pre-line;
|
|
||||||
word-break: break-all;
|
|
||||||
padding: 15px 60px 15px 15px;
|
|
||||||
margin-left: 45px;
|
|
||||||
display: block;
|
|
||||||
line-height: 1.2;
|
|
||||||
transition: color 0.4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .todo-list li.completed label {
|
|
||||||
color: #d9d9d9;
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .footer {
|
|
||||||
color: #777;
|
|
||||||
padding: 10px 15px;
|
|
||||||
height: 20px;
|
|
||||||
text-align: center;
|
|
||||||
border-top: 1px solid #e6e6e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .footer:before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 50px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
|
|
||||||
0 8px 0 -3px #f6f6f6,
|
|
||||||
0 9px 1px -3px rgba(0, 0, 0, 0.2),
|
|
||||||
0 16px 0 -6px #f6f6f6,
|
|
||||||
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .todo-count {
|
|
||||||
float: left;
|
|
||||||
text-align: left;
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .toggle-menu {
|
|
||||||
position: absolute;
|
|
||||||
right: 15px;
|
|
||||||
font-weight: 300;
|
|
||||||
color: rgba(175, 47, 47, 0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .filters {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .filters li {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .filters li a {
|
|
||||||
color: inherit;
|
|
||||||
margin: 3px;
|
|
||||||
padding: 3px 7px;
|
|
||||||
text-decoration: none;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .filters li a.selected,
|
|
||||||
.filters li a:hover {
|
|
||||||
border-color: rgba(175, 47, 47, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .filters li a.selected {
|
|
||||||
border-color: rgba(175, 47, 47, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Hack to remove background from Mobile Safari.
|
|
||||||
Can't use it globally since it destroys checkboxes in Firefox
|
|
||||||
*/
|
|
||||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
|
||||||
.todoapp .toggle-all,
|
|
||||||
.todoapp .todo-list li .toggle {
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .todo-list li .toggle {
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .toggle-all {
|
|
||||||
-webkit-transform: rotate(90deg);
|
|
||||||
transform: rotate(90deg);
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 430px) {
|
|
||||||
.todoapp .footer {
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todoapp .filters {
|
|
||||||
bottom: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<dom-module id='ha-panel-react'>
|
|
||||||
<template>
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
background: #f5f5f5;
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
.mount {
|
|
||||||
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.4em;
|
|
||||||
color: #4d4d4d;
|
|
||||||
min-width: 230px;
|
|
||||||
max-width: 550px;
|
|
||||||
margin: 0 auto;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-font-smoothing: antialiased;
|
|
||||||
font-smoothing: antialiased;
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div id='mount' class='mount'></div>
|
|
||||||
</template>
|
|
||||||
</dom-module>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Example uses ES6. Will only work in modern browsers
|
|
||||||
class TodoMVC extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
filter: 'all',
|
|
||||||
// load initial value of entities
|
|
||||||
entities: this.props.hass.reactor.evaluate(
|
|
||||||
this.props.hass.entityGetters.visibleEntityMap),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// register to entity updates
|
|
||||||
this._unwatchHass = this.props.hass.reactor.observe(
|
|
||||||
this.props.hass.entityGetters.visibleEntityMap,
|
|
||||||
entities => this.setState({entities}))
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
// unregister to entity updates
|
|
||||||
this._unwatchHass();
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePickFilter(filter, ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.setState({filter});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEntityToggle(entity, ev) {
|
|
||||||
this.props.hass.serviceActions.callService(
|
|
||||||
entity.domain, 'toggle', { entity_id: entity.entityId });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleToggleMenu(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
Polymer.Base.fire('open-menu', null, {node: ev.target});
|
|
||||||
}
|
|
||||||
|
|
||||||
entityRow(entity) {
|
|
||||||
const completed = entity.state === 'on';
|
|
||||||
|
|
||||||
return React.createElement(
|
|
||||||
'li', {
|
|
||||||
className: completed && 'completed',
|
|
||||||
key: entity.entityId,
|
|
||||||
},
|
|
||||||
React.createElement(
|
|
||||||
"div", { className: "view" },
|
|
||||||
React.createElement(
|
|
||||||
"input", {
|
|
||||||
checked: completed,
|
|
||||||
className: "toggle",
|
|
||||||
type: "checkbox",
|
|
||||||
onChange: ev => this.handleEntityToggle(entity, ev),
|
|
||||||
}),
|
|
||||||
React.createElement("label", null, entity.entityDisplay)));
|
|
||||||
}
|
|
||||||
|
|
||||||
filterRow(filter) {
|
|
||||||
return React.createElement(
|
|
||||||
"li", { key: filter },
|
|
||||||
React.createElement(
|
|
||||||
"a", {
|
|
||||||
href: "#",
|
|
||||||
className: this.state.filter === filter && "selected",
|
|
||||||
onClick: ev => this.handlePickFilter(filter, ev),
|
|
||||||
},
|
|
||||||
filter.substring(0, 1).toUpperCase() + filter.substring(1)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { entities, filter } = this.state;
|
|
||||||
|
|
||||||
if (!entities) return null;
|
|
||||||
|
|
||||||
const filters = ['all', 'light', 'switch'];
|
|
||||||
|
|
||||||
const showEntities = filter === 'all' ?
|
|
||||||
entities.filter(ent => filters.includes(ent.domain)) :
|
|
||||||
entities.filter(ent => ent.domain == filter);
|
|
||||||
|
|
||||||
return React.createElement(
|
|
||||||
'div', { className: 'todoapp-wrapper' },
|
|
||||||
React.createElement(
|
|
||||||
"section", { className: "todoapp" },
|
|
||||||
React.createElement(
|
|
||||||
"div", null,
|
|
||||||
React.createElement(
|
|
||||||
"header", { className: "header" },
|
|
||||||
React.createElement("h1", null, this.props.title || "todos")
|
|
||||||
),
|
|
||||||
React.createElement(
|
|
||||||
"section", { className: "main" },
|
|
||||||
React.createElement(
|
|
||||||
"ul", { className: "todo-list" },
|
|
||||||
showEntities.valueSeq().map(ent => this.entityRow(ent)))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
React.createElement(
|
|
||||||
"footer", { className: "footer" },
|
|
||||||
React.createElement(
|
|
||||||
"span", { className: "todo-count" },
|
|
||||||
showEntities.filter(ent => ent.state === 'off').size + " items left"
|
|
||||||
),
|
|
||||||
React.createElement(
|
|
||||||
"ul", { className: "filters" },
|
|
||||||
filters.map(filter => this.filterRow(filter))
|
|
||||||
),
|
|
||||||
!this.props.showMenu && React.createElement(
|
|
||||||
"a", {
|
|
||||||
className: "toggle-menu",
|
|
||||||
href: '#',
|
|
||||||
onClick: ev => this.handleToggleMenu(ev),
|
|
||||||
},
|
|
||||||
"Show menu"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Polymer({
|
|
||||||
is: 'ha-panel-react',
|
|
||||||
|
|
||||||
properties: {
|
|
||||||
// Home Assistant object
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
// If should render in narrow mode
|
|
||||||
narrow: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
// If sidebar is currently shown
|
|
||||||
showMenu: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Home Assistant panel info
|
|
||||||
// panel.config contains config passed to register_panel serverside
|
|
||||||
panel: {
|
|
||||||
type: Object,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// This will make sure we forward changed properties to React
|
|
||||||
observers: [
|
|
||||||
'propsChanged(hass, narrow, showMenu, panel)',
|
|
||||||
],
|
|
||||||
|
|
||||||
// Mount React when element attached
|
|
||||||
attached: function () {
|
|
||||||
this.mount(this.hass, this.narrow, this.showMenu, this.panel);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Called when properties change
|
|
||||||
propsChanged: function (hass, narrow, showMenu, panel) {
|
|
||||||
this.mount(hass, narrow, showMenu, panel);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Render React. Debounce in case multiple properties change.
|
|
||||||
mount: function (hass, narrow, showMenu, panel) {
|
|
||||||
this.debounce('mount', function () {
|
|
||||||
ReactDOM.render(React.createElement(TodoMVC, {
|
|
||||||
hass: hass,
|
|
||||||
narrow: narrow,
|
|
||||||
showMenu: showMenu,
|
|
||||||
title: panel.config ? panel.config.title : null
|
|
||||||
}), this.$.mount);
|
|
||||||
}.bind(this));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Unmount React node when panel no longer in use.
|
|
||||||
detached: function () {
|
|
||||||
ReactDOM.unmountComponentAtNode(this.$.mount);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
|
@ -20,6 +20,17 @@ from homeassistant.const import (
|
||||||
from homeassistant.util.async import run_callback_threadsafe
|
from homeassistant.util.async import run_callback_threadsafe
|
||||||
|
|
||||||
|
|
||||||
|
def attempt_use_uvloop():
|
||||||
|
"""Attempt to use uvloop."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
try:
|
||||||
|
import uvloop
|
||||||
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def monkey_patch_asyncio():
|
def monkey_patch_asyncio():
|
||||||
"""Replace weakref.WeakSet to address Python 3 bug.
|
"""Replace weakref.WeakSet to address Python 3 bug.
|
||||||
|
|
||||||
|
@ -311,8 +322,7 @@ def setup_and_run_hass(config_dir: str,
|
||||||
EVENT_HOMEASSISTANT_START, open_browser
|
EVENT_HOMEASSISTANT_START, open_browser
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.start()
|
return hass.start()
|
||||||
return hass.exit_code
|
|
||||||
|
|
||||||
|
|
||||||
def try_to_restart() -> None:
|
def try_to_restart() -> None:
|
||||||
|
@ -359,11 +369,13 @@ def try_to_restart() -> None:
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
"""Start Home Assistant."""
|
"""Start Home Assistant."""
|
||||||
|
validate_python()
|
||||||
|
|
||||||
|
attempt_use_uvloop()
|
||||||
|
|
||||||
if sys.version_info[:3] < (3, 5, 3):
|
if sys.version_info[:3] < (3, 5, 3):
|
||||||
monkey_patch_asyncio()
|
monkey_patch_asyncio()
|
||||||
|
|
||||||
validate_python()
|
|
||||||
|
|
||||||
args = get_arguments()
|
args = get_arguments()
|
||||||
|
|
||||||
if args.script is not None:
|
if args.script is not None:
|
||||||
|
|
|
@ -74,8 +74,6 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
start = time()
|
start = time()
|
||||||
hass.async_track_tasks()
|
|
||||||
|
|
||||||
core_config = config.get(core.DOMAIN, {})
|
core_config = config.get(core.DOMAIN, {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -140,10 +138,10 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||||
continue
|
continue
|
||||||
hass.async_add_job(async_setup_component(hass, component, config))
|
hass.async_add_job(async_setup_component(hass, component, config))
|
||||||
|
|
||||||
yield from hass.async_stop_track_tasks()
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
stop = time()
|
stop = time()
|
||||||
_LOGGER.info('Home Assistant initialized in %ss', round(stop-start, 2))
|
_LOGGER.info('Home Assistant initialized in %.2fs', stop-start)
|
||||||
|
|
||||||
async_register_signal_handling(hass)
|
async_register_signal_handling(hass)
|
||||||
return hass
|
return hass
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Interfaces with Alarm.com alarm control panels.
|
Interfaces with Alarm.com 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.alarmdotcom/
|
https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import asyncio
|
||||||
import voluptuous as vol
|
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 (
|
||||||
|
@ -15,10 +15,9 @@ from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN, CONF_CODE,
|
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN, CONF_CODE,
|
||||||
CONF_NAME)
|
CONF_NAME)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/Xorso/pyalarmdotcom'
|
REQUIREMENTS = ['pyalarmdotcom==0.2.9']
|
||||||
'/archive/0.1.1.zip'
|
|
||||||
'#pyalarmdotcom==0.1.1']
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -32,14 +31,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
@asyncio.coroutine
|
||||||
"""Setup an Alarm.com control panel."""
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Setup a Alarm.com control panel."""
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
code = config.get(CONF_CODE)
|
code = config.get(CONF_CODE)
|
||||||
username = config.get(CONF_USERNAME)
|
username = config.get(CONF_USERNAME)
|
||||||
password = config.get(CONF_PASSWORD)
|
password = config.get(CONF_PASSWORD)
|
||||||
|
|
||||||
add_devices([AlarmDotCom(hass, name, code, username, password)], True)
|
alarmdotcom = AlarmDotCom(hass, name, code, username, password)
|
||||||
|
yield from alarmdotcom.async_login()
|
||||||
|
async_add_devices([alarmdotcom])
|
||||||
|
|
||||||
|
|
||||||
class AlarmDotCom(alarm.AlarmControlPanel):
|
class AlarmDotCom(alarm.AlarmControlPanel):
|
||||||
|
@ -47,18 +49,30 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||||
|
|
||||||
def __init__(self, hass, name, code, username, password):
|
def __init__(self, hass, name, code, username, password):
|
||||||
"""Initialize the Alarm.com status."""
|
"""Initialize the Alarm.com status."""
|
||||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
from pyalarmdotcom import Alarmdotcom
|
||||||
self._alarm = Alarmdotcom(username, password, timeout=10)
|
_LOGGER.debug('Setting up Alarm.com...')
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
self._code = str(code) if code else None
|
self._code = str(code) if code else None
|
||||||
self._username = username
|
self._username = username
|
||||||
self._password = password
|
self._password = password
|
||||||
|
self._websession = async_get_clientsession(self._hass)
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
|
self._alarm = Alarmdotcom(username,
|
||||||
|
password,
|
||||||
|
self._websession,
|
||||||
|
hass.loop)
|
||||||
|
|
||||||
def update(self):
|
@asyncio.coroutine
|
||||||
|
def async_login(self):
|
||||||
|
"""Login to Alarm.com."""
|
||||||
|
yield from self._alarm.async_login()
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_update(self):
|
||||||
"""Fetch the latest state."""
|
"""Fetch the latest state."""
|
||||||
self._state = self._alarm.state
|
yield from self._alarm.async_update()
|
||||||
|
return self._alarm.state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -73,45 +87,36 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
if self._state == 'Disarmed':
|
if self._alarm.state.lower() == 'disarmed':
|
||||||
return STATE_ALARM_DISARMED
|
return STATE_ALARM_DISARMED
|
||||||
elif self._state == 'Armed Stay':
|
elif self._alarm.state.lower() == 'armed stay':
|
||||||
return STATE_ALARM_ARMED_HOME
|
return STATE_ALARM_ARMED_HOME
|
||||||
elif self._state == 'Armed Away':
|
elif self._alarm.state.lower() == 'armed away':
|
||||||
return STATE_ALARM_ARMED_AWAY
|
return STATE_ALARM_ARMED_AWAY
|
||||||
else:
|
else:
|
||||||
return STATE_UNKNOWN
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
def alarm_disarm(self, code=None):
|
@asyncio.coroutine
|
||||||
|
def async_alarm_disarm(self, code=None):
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
if not self._validate_code(code, 'disarming home'):
|
if self._validate_code(code):
|
||||||
return
|
yield from self._alarm.async_alarm_disarm()
|
||||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
|
||||||
# Open another session to alarm.com to fire off the command
|
|
||||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
|
||||||
_alarm.disarm()
|
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
@asyncio.coroutine
|
||||||
"""Send arm home command."""
|
def async_alarm_arm_home(self, code=None):
|
||||||
if not self._validate_code(code, 'arming home'):
|
"""Send arm hom command."""
|
||||||
return
|
if self._validate_code(code):
|
||||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
yield from self._alarm.async_alarm_arm_home()
|
||||||
# Open another session to alarm.com to fire off the command
|
|
||||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
|
||||||
_alarm.arm_stay()
|
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
@asyncio.coroutine
|
||||||
|
def async_alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
if not self._validate_code(code, 'arming home'):
|
if self._validate_code(code):
|
||||||
return
|
yield from self._alarm.async_alarm_arm_away()
|
||||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
|
||||||
# Open another session to alarm.com to fire off the command
|
|
||||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
|
||||||
_alarm.arm_away()
|
|
||||||
|
|
||||||
def _validate_code(self, code, state):
|
def _validate_code(self, code):
|
||||||
"""Validate given code."""
|
"""Validate given code."""
|
||||||
check = self._code is None or code == self._code
|
check = self._code is None or code == self._code
|
||||||
if not check:
|
if not check:
|
||||||
_LOGGER.warning('Wrong code entered for %s', state)
|
_LOGGER.warning('Wrong code entered.')
|
||||||
return check
|
return check
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
"""Interfaces with TotalConnect alarm control panels."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
|
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN,
|
||||||
|
CONF_NAME)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
REQUIREMENTS = ['total_connect_client==0.7']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'Total Connect'
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup a TotalConnect control panel."""
|
||||||
|
name = config.get(CONF_NAME)
|
||||||
|
username = config.get(CONF_USERNAME)
|
||||||
|
password = config.get(CONF_PASSWORD)
|
||||||
|
|
||||||
|
total_connect = TotalConnect(name, username, password)
|
||||||
|
add_devices([total_connect], True)
|
||||||
|
|
||||||
|
|
||||||
|
class TotalConnect(alarm.AlarmControlPanel):
|
||||||
|
"""Represent an TotalConnect status."""
|
||||||
|
|
||||||
|
def __init__(self, name, username, password):
|
||||||
|
"""Initialize the TotalConnect status."""
|
||||||
|
from total_connect_client import TotalConnectClient
|
||||||
|
|
||||||
|
_LOGGER.debug('Setting up TotalConnect...')
|
||||||
|
self._name = name
|
||||||
|
self._username = username
|
||||||
|
self._password = password
|
||||||
|
self._state = STATE_UNKNOWN
|
||||||
|
self._client = TotalConnectClient.TotalConnectClient(username,
|
||||||
|
password)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
status = self._client.get_armed_status()
|
||||||
|
|
||||||
|
if status == self._client.DISARMED:
|
||||||
|
state = STATE_ALARM_DISARMED
|
||||||
|
elif status == self._client.ARMED_STAY:
|
||||||
|
state = STATE_ALARM_ARMED_HOME
|
||||||
|
elif status == self._client.ARMED_AWAY:
|
||||||
|
state = STATE_ALARM_ARMED_AWAY
|
||||||
|
else:
|
||||||
|
state = STATE_UNKNOWN
|
||||||
|
|
||||||
|
self._state = state
|
||||||
|
|
||||||
|
def alarm_disarm(self, code=None):
|
||||||
|
"""Send disarm command."""
|
||||||
|
self._client.disarm()
|
||||||
|
|
||||||
|
def alarm_arm_home(self, code=None):
|
||||||
|
"""Send arm home command."""
|
||||||
|
self._client.arm_stay()
|
||||||
|
|
||||||
|
def alarm_arm_away(self, code=None):
|
||||||
|
"""Send arm away command."""
|
||||||
|
self._client.arm_away()
|
|
@ -26,17 +26,24 @@ from homeassistant.util.dt import utcnow
|
||||||
from homeassistant.components.camera.mjpeg import (
|
from homeassistant.components.camera.mjpeg import (
|
||||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
|
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
|
||||||
|
|
||||||
DOMAIN = 'android_ip_webcam'
|
REQUIREMENTS = ['pydroid-ipcam==0.8']
|
||||||
REQUIREMENTS = ["pydroid-ipcam==0.6"]
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
SCAN_INTERVAL = timedelta(seconds=10)
|
|
||||||
|
|
||||||
DATA_IP_WEBCAM = 'android_ip_webcam'
|
|
||||||
|
|
||||||
|
ATTR_AUD_CONNS = 'Audio Connections'
|
||||||
ATTR_HOST = 'host'
|
ATTR_HOST = 'host'
|
||||||
ATTR_VID_CONNS = 'Video Connections'
|
ATTR_VID_CONNS = 'Video Connections'
|
||||||
ATTR_AUD_CONNS = 'Audio Connections'
|
|
||||||
|
CONF_MOTION_SENSOR = 'motion_sensor'
|
||||||
|
|
||||||
|
DATA_IP_WEBCAM = 'android_ip_webcam'
|
||||||
|
DEFAULT_NAME = 'IP Webcam'
|
||||||
|
DEFAULT_PORT = 8080
|
||||||
|
DEFAULT_TIMEOUT = 10
|
||||||
|
DOMAIN = 'android_ip_webcam'
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=10)
|
||||||
|
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
|
||||||
|
|
||||||
KEY_MAP = {
|
KEY_MAP = {
|
||||||
'audio_connections': 'Audio Connections',
|
'audio_connections': 'Audio Connections',
|
||||||
|
@ -123,15 +130,6 @@ SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
|
||||||
'battery_voltage', 'light', 'motion', 'pressure', 'proximity',
|
'battery_voltage', 'light', 'motion', 'pressure', 'proximity',
|
||||||
'sound', 'video_connections']
|
'sound', 'video_connections']
|
||||||
|
|
||||||
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
|
|
||||||
|
|
||||||
CONF_MOTION_SENSOR = 'motion_sensor'
|
|
||||||
|
|
||||||
DEFAULT_NAME = 'IP Webcam'
|
|
||||||
DEFAULT_PORT = 8080
|
|
||||||
DEFAULT_TIMEOUT = 10
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
@ -153,7 +151,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup(hass, config):
|
def async_setup(hass, config):
|
||||||
"""Setup the IP Webcam component."""
|
"""Set up the IP Webcam component."""
|
||||||
from pydroid_ipcam import PyDroidIPCam
|
from pydroid_ipcam import PyDroidIPCam
|
||||||
|
|
||||||
webcams = hass.data[DATA_IP_WEBCAM] = {}
|
webcams = hass.data[DATA_IP_WEBCAM] = {}
|
||||||
|
@ -161,7 +159,7 @@ def async_setup(hass, config):
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_ipcamera(cam_config):
|
def async_setup_ipcamera(cam_config):
|
||||||
"""Setup a ip camera."""
|
"""Set up an IP camera."""
|
||||||
host = cam_config[CONF_HOST]
|
host = cam_config[CONF_HOST]
|
||||||
username = cam_config.get(CONF_USERNAME)
|
username = cam_config.get(CONF_USERNAME)
|
||||||
password = cam_config.get(CONF_PASSWORD)
|
password = cam_config.get(CONF_PASSWORD)
|
||||||
|
@ -171,7 +169,7 @@ def async_setup(hass, config):
|
||||||
sensors = cam_config[CONF_SENSORS]
|
sensors = cam_config[CONF_SENSORS]
|
||||||
motion = cam_config[CONF_MOTION_SENSOR]
|
motion = cam_config[CONF_MOTION_SENSOR]
|
||||||
|
|
||||||
# init ip webcam
|
# Init ip webcam
|
||||||
cam = PyDroidIPCam(
|
cam = PyDroidIPCam(
|
||||||
hass.loop, websession, host, cam_config[CONF_PORT],
|
hass.loop, websession, host, cam_config[CONF_PORT],
|
||||||
username=username, password=password,
|
username=username, password=password,
|
||||||
|
@ -192,7 +190,7 @@ def async_setup(hass, config):
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_update_data(now):
|
def async_update_data(now):
|
||||||
"""Update data from ipcam in SCAN_INTERVAL."""
|
"""Update data from IP camera in SCAN_INTERVAL."""
|
||||||
yield from cam.update()
|
yield from cam.update()
|
||||||
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
|
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
|
||||||
|
|
||||||
|
@ -201,7 +199,7 @@ def async_setup(hass, config):
|
||||||
|
|
||||||
yield from async_update_data(None)
|
yield from async_update_data(None)
|
||||||
|
|
||||||
# load platforms
|
# Load platforms
|
||||||
webcams[host] = cam
|
webcams[host] = cam
|
||||||
|
|
||||||
mjpeg_camera = {
|
mjpeg_camera = {
|
||||||
|
|
|
@ -50,9 +50,11 @@ def setup(hass, config):
|
||||||
hass.http.register_view(APIDomainServicesView)
|
hass.http.register_view(APIDomainServicesView)
|
||||||
hass.http.register_view(APIEventForwardingView)
|
hass.http.register_view(APIEventForwardingView)
|
||||||
hass.http.register_view(APIComponentsView)
|
hass.http.register_view(APIComponentsView)
|
||||||
hass.http.register_view(APIErrorLogView)
|
|
||||||
hass.http.register_view(APITemplateView)
|
hass.http.register_view(APITemplateView)
|
||||||
|
|
||||||
|
hass.http.register_static_path(
|
||||||
|
URL_API_ERROR_LOG, hass.config.path(ERROR_LOG_FILENAME), False)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -402,20 +404,6 @@ class APIComponentsView(HomeAssistantView):
|
||||||
return self.json(request.app['hass'].config.components)
|
return self.json(request.app['hass'].config.components)
|
||||||
|
|
||||||
|
|
||||||
class APIErrorLogView(HomeAssistantView):
|
|
||||||
"""View to handle ErrorLog requests."""
|
|
||||||
|
|
||||||
url = URL_API_ERROR_LOG
|
|
||||||
name = "api:error-log"
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def get(self, request):
|
|
||||||
"""Serve error log."""
|
|
||||||
resp = yield from self.file(
|
|
||||||
request, request.app['hass'].config.path(ERROR_LOG_FILENAME))
|
|
||||||
return resp
|
|
||||||
|
|
||||||
|
|
||||||
class APITemplateView(HomeAssistantView):
|
class APITemplateView(HomeAssistantView):
|
||||||
"""View to handle requests."""
|
"""View to handle requests."""
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,11 @@ import os
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.setup import async_prepare_setup_platform
|
from homeassistant.setup import async_prepare_setup_platform
|
||||||
|
from homeassistant.core import CoreState
|
||||||
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,
|
||||||
SERVICE_TOGGLE, SERVICE_RELOAD)
|
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START)
|
||||||
from homeassistant.components import logbook
|
from homeassistant.components import logbook
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import extract_domain_configs, script, condition
|
from homeassistant.helpers import extract_domain_configs, script, condition
|
||||||
|
@ -81,8 +82,7 @@ _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.Schema({
|
PLATFORM_SCHEMA = vol.Schema({
|
||||||
CONF_ALIAS: cv.string,
|
CONF_ALIAS: cv.string,
|
||||||
vol.Optional(CONF_INITIAL_STATE,
|
vol.Optional(CONF_INITIAL_STATE): cv.boolean,
|
||||||
default=DEFAULT_INITIAL_STATE): cv.boolean,
|
|
||||||
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
|
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
|
||||||
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
|
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
|
||||||
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
|
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
|
||||||
|
@ -101,15 +101,13 @@ TRIGGER_SERVICE_SCHEMA = vol.Schema({
|
||||||
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
||||||
|
|
||||||
|
|
||||||
def is_on(hass, entity_id=None):
|
def is_on(hass, entity_id):
|
||||||
"""
|
"""
|
||||||
Return true if specified automation entity_id is on.
|
Return true if specified automation entity_id is on.
|
||||||
|
|
||||||
Check all automation if no entity_id specified.
|
Async friendly.
|
||||||
"""
|
"""
|
||||||
entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN)
|
return hass.states.is_state(entity_id, STATE_ON)
|
||||||
return any(hass.states.is_state(entity_id, STATE_ON)
|
|
||||||
for entity_id in entity_ids)
|
|
||||||
|
|
||||||
|
|
||||||
def turn_on(hass, entity_id=None):
|
def turn_on(hass, entity_id=None):
|
||||||
|
@ -231,7 +229,6 @@ class AutomationEntity(ToggleEntity):
|
||||||
self._async_detach_triggers = None
|
self._async_detach_triggers = None
|
||||||
self._cond_func = cond_func
|
self._cond_func = cond_func
|
||||||
self._async_action = async_action
|
self._async_action = async_action
|
||||||
self._enabled = False
|
|
||||||
self._last_triggered = None
|
self._last_triggered = None
|
||||||
self._hidden = hidden
|
self._hidden = hidden
|
||||||
self._initial_state = initial_state
|
self._initial_state = initial_state
|
||||||
|
@ -261,38 +258,54 @@ class AutomationEntity(ToggleEntity):
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return True if entity is on."""
|
"""Return True if entity is on."""
|
||||||
return self._enabled
|
return self._async_detach_triggers is not None
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self) -> None:
|
def async_added_to_hass(self) -> None:
|
||||||
"""Startup with initial state or previous state."""
|
"""Startup with initial state or previous state."""
|
||||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
enable_automation = DEFAULT_INITIAL_STATE
|
||||||
if state is None:
|
|
||||||
if self._initial_state:
|
if self._initial_state is not None:
|
||||||
yield from self.async_enable()
|
enable_automation = self._initial_state
|
||||||
else:
|
else:
|
||||||
|
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||||
|
if state:
|
||||||
|
enable_automation = state.state == STATE_ON
|
||||||
self._last_triggered = state.attributes.get('last_triggered')
|
self._last_triggered = state.attributes.get('last_triggered')
|
||||||
if state.state == STATE_ON:
|
|
||||||
|
if not enable_automation:
|
||||||
|
return
|
||||||
|
|
||||||
|
# HomeAssistant is starting up
|
||||||
|
elif self.hass.state == CoreState.not_running:
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_enable_automation(event):
|
||||||
|
"""Start automation on startup."""
|
||||||
|
yield from self.async_enable()
|
||||||
|
|
||||||
|
self.hass.bus.async_listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_START, async_enable_automation)
|
||||||
|
|
||||||
|
# HomeAssistant is running
|
||||||
|
else:
|
||||||
yield from self.async_enable()
|
yield from self.async_enable()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_turn_on(self, **kwargs) -> None:
|
def async_turn_on(self, **kwargs) -> None:
|
||||||
"""Turn the entity on and update the state."""
|
"""Turn the entity on and update the state."""
|
||||||
if self._enabled:
|
if self.is_on:
|
||||||
return
|
return
|
||||||
|
|
||||||
yield from self.async_enable()
|
yield from self.async_enable()
|
||||||
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:
|
||||||
"""Turn the entity off."""
|
"""Turn the entity off."""
|
||||||
if not self._enabled:
|
if not self.is_on:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._async_detach_triggers()
|
self._async_detach_triggers()
|
||||||
self._async_detach_triggers = None
|
self._async_detach_triggers = None
|
||||||
self._enabled = False
|
|
||||||
yield from self.async_update_ha_state()
|
yield from self.async_update_ha_state()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -318,12 +331,12 @@ class AutomationEntity(ToggleEntity):
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
if self._enabled:
|
if self.is_on:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._async_detach_triggers = yield from self._async_attach_triggers(
|
self._async_detach_triggers = yield from self._async_attach_triggers(
|
||||||
self.async_trigger)
|
self.async_trigger)
|
||||||
self._enabled = True
|
yield from self.async_update_ha_state()
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -342,7 +355,7 @@ def _async_process_config(hass, config, component):
|
||||||
list_no)
|
list_no)
|
||||||
|
|
||||||
hidden = config_block[CONF_HIDE_ENTITY]
|
hidden = config_block[CONF_HIDE_ENTITY]
|
||||||
initial_state = config_block[CONF_INITIAL_STATE]
|
initial_state = config_block.get(CONF_INITIAL_STATE)
|
||||||
|
|
||||||
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
|
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
|
||||||
name)
|
name)
|
||||||
|
|
|
@ -2,15 +2,15 @@
|
||||||
Offer event listening automation rules.
|
Offer event listening automation rules.
|
||||||
|
|
||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/components/automation/#event-trigger
|
at https://home-assistant.io/docs/automation/trigger/#event-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback, CoreState
|
||||||
from homeassistant.const import CONF_PLATFORM
|
from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_START
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
CONF_EVENT_TYPE = "event_type"
|
CONF_EVENT_TYPE = "event_type"
|
||||||
|
@ -31,6 +31,19 @@ def async_trigger(hass, config, action):
|
||||||
event_type = config.get(CONF_EVENT_TYPE)
|
event_type = config.get(CONF_EVENT_TYPE)
|
||||||
event_data = config.get(CONF_EVENT_DATA)
|
event_data = config.get(CONF_EVENT_DATA)
|
||||||
|
|
||||||
|
if (event_type == EVENT_HOMEASSISTANT_START and
|
||||||
|
hass.state == CoreState.starting):
|
||||||
|
_LOGGER.warning('Deprecation: Automations should not listen to event '
|
||||||
|
"'homeassistant_start'. Use platform 'homeassistant' "
|
||||||
|
'instead. Feature will be removed in 0.45')
|
||||||
|
hass.async_run_job(action, {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'event',
|
||||||
|
'event': None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return lambda: None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def handle_event(event):
|
def handle_event(event):
|
||||||
"""Listen for events and calls the action when data matches."""
|
"""Listen for events and calls the action when data matches."""
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
"""
|
||||||
|
Offer Home Assistant core automation rules.
|
||||||
|
|
||||||
|
For more details about this automation rule, please refer to the documentation
|
||||||
|
at https://home-assistant.io/components/automation/#homeassistant-trigger
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback, CoreState
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP)
|
||||||
|
|
||||||
|
EVENT_START = 'start'
|
||||||
|
EVENT_SHUTDOWN = 'shutdown'
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TRIGGER_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(CONF_PLATFORM): 'homeassistant',
|
||||||
|
vol.Required(CONF_EVENT): vol.Any(EVENT_START, EVENT_SHUTDOWN),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_trigger(hass, config, action):
|
||||||
|
"""Listen for events based on configuration."""
|
||||||
|
event = config.get(CONF_EVENT)
|
||||||
|
|
||||||
|
if event == EVENT_SHUTDOWN:
|
||||||
|
@callback
|
||||||
|
def hass_shutdown(event):
|
||||||
|
"""Called when Home Assistant is shutting down."""
|
||||||
|
hass.async_run_job(action, {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'homeassistant',
|
||||||
|
'event': event,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||||
|
hass_shutdown)
|
||||||
|
|
||||||
|
# Automation are enabled while hass is starting up, fire right away
|
||||||
|
# Check state because a config reload shouldn't trigger it.
|
||||||
|
elif hass.state == CoreState.starting:
|
||||||
|
hass.async_run_job(action, {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'homeassistant',
|
||||||
|
'event': event,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return lambda: None
|
|
@ -70,7 +70,7 @@ def async_trigger(hass, config, action):
|
||||||
nonlocal held_less_than, held_more_than
|
nonlocal held_less_than, held_more_than
|
||||||
pressed_time = dt_util.utcnow()
|
pressed_time = dt_util.utcnow()
|
||||||
if held_more_than is None and held_less_than is None:
|
if held_more_than is None and held_less_than is None:
|
||||||
call_action()
|
hass.add_job(call_action)
|
||||||
if held_more_than is not None and held_less_than is None:
|
if held_more_than is not None and held_less_than is None:
|
||||||
cancel_pressed_more_than = track_point_in_utc_time(
|
cancel_pressed_more_than = track_point_in_utc_time(
|
||||||
hass,
|
hass,
|
||||||
|
@ -88,7 +88,7 @@ def async_trigger(hass, config, action):
|
||||||
held_time = dt_util.utcnow() - pressed_time
|
held_time = dt_util.utcnow() - pressed_time
|
||||||
if held_less_than is not None and held_time < held_less_than:
|
if held_less_than is not None and held_time < held_less_than:
|
||||||
if held_more_than is None or held_time > held_more_than:
|
if held_more_than is None or held_time > held_more_than:
|
||||||
call_action()
|
hass.add_job(call_action)
|
||||||
|
|
||||||
hass.data['litejet_system'].on_switch_pressed(number, pressed)
|
hass.data['litejet_system'].on_switch_pressed(number, pressed)
|
||||||
hass.data['litejet_system'].on_switch_released(number, released)
|
hass.data['litejet_system'].on_switch_released(number, released)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
Offer MQTT listening automation rules.
|
Offer MQTT listening automation rules.
|
||||||
|
|
||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/components/automation/#mqtt-trigger
|
at https://home-assistant.io/docs/automation/trigger/#mqtt-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
Offer numeric state listening automation rules.
|
Offer numeric state listening automation rules.
|
||||||
|
|
||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/components/automation/#numeric-state-trigger
|
at https://home-assistant.io/docs/automation/trigger/#numeric-state-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
Offer state listening automation rules.
|
Offer state listening automation rules.
|
||||||
|
|
||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/components/automation/#state-trigger
|
at https://home-assistant.io/docs/automation/trigger/#state-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
Offer sun based automation rules.
|
Offer sun based automation rules.
|
||||||
|
|
||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/components/automation/#sun-trigger
|
at https://home-assistant.io/docs/automation/trigger/#sun-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
Offer template automation rules.
|
Offer template automation rules.
|
||||||
|
|
||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/components/automation/#template-trigger
|
at https://home-assistant.io/docs/automation/trigger/#template-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
Offer time listening automation rules.
|
Offer time listening automation rules.
|
||||||
|
|
||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/components/automation/#time-trigger
|
at https://home-assistant.io/docs/automation/trigger/#time-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
Offer zone automation rules.
|
Offer zone automation rules.
|
||||||
|
|
||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/components/automation/#zone-trigger
|
at https://home-assistant.io/docs/automation/trigger/#zone-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
|
@ -11,7 +11,7 @@ DEPENDENCIES = ['blink']
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the blink binary sensors."""
|
"""Set up the blink binary sensors."""
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||||
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||||
|
|
||||||
REQUIREMENTS = ['pyhik==0.1.0']
|
REQUIREMENTS = ['pyhik==0.1.2']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_IGNORED = 'ignored'
|
CONF_IGNORED = 'ignored'
|
||||||
|
@ -33,7 +33,6 @@ ATTR_DELAY = 'delay'
|
||||||
DEVICE_CLASS_MAP = {
|
DEVICE_CLASS_MAP = {
|
||||||
'Motion': 'motion',
|
'Motion': 'motion',
|
||||||
'Line Crossing': 'motion',
|
'Line Crossing': 'motion',
|
||||||
'IO Trigger': None,
|
|
||||||
'Field Detection': 'motion',
|
'Field Detection': 'motion',
|
||||||
'Video Loss': None,
|
'Video Loss': None,
|
||||||
'Tamper Detection': 'motion',
|
'Tamper Detection': 'motion',
|
||||||
|
@ -47,6 +46,7 @@ DEVICE_CLASS_MAP = {
|
||||||
'Bad Video': None,
|
'Bad Video': None,
|
||||||
'PIR Alarm': 'motion',
|
'PIR Alarm': 'motion',
|
||||||
'Face Detection': 'motion',
|
'Face Detection': 'motion',
|
||||||
|
'Scene Change Detection': 'motion',
|
||||||
}
|
}
|
||||||
|
|
||||||
CUSTOMIZE_SCHEMA = vol.Schema({
|
CUSTOMIZE_SCHEMA = vol.Schema({
|
||||||
|
@ -91,8 +91,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
||||||
entities = []
|
entities = []
|
||||||
|
|
||||||
for sensor in data.sensors:
|
for sensor, channel_list in data.sensors.items():
|
||||||
|
for channel in channel_list:
|
||||||
# Build sensor name, then parse customize config.
|
# Build sensor name, then parse customize config.
|
||||||
|
if data.type == 'NVR':
|
||||||
|
sensor_name = '{}_{}'.format(
|
||||||
|
sensor.replace(' ', '_'), channel[1])
|
||||||
|
else:
|
||||||
sensor_name = sensor.replace(' ', '_')
|
sensor_name = sensor.replace(' ', '_')
|
||||||
|
|
||||||
custom = customize.get(sensor_name.lower(), {})
|
custom = customize.get(sensor_name.lower(), {})
|
||||||
|
@ -102,13 +107,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
_LOGGER.debug('Entity: %s - %s, Options - Ignore: %s, Delay: %s',
|
_LOGGER.debug('Entity: %s - %s, Options - Ignore: %s, Delay: %s',
|
||||||
data.name, sensor_name, ignore, delay)
|
data.name, sensor_name, ignore, delay)
|
||||||
if not ignore:
|
if not ignore:
|
||||||
entities.append(HikvisionBinarySensor(hass, sensor, data, delay))
|
entities.append(HikvisionBinarySensor(
|
||||||
|
hass, sensor, channel[1], data, delay))
|
||||||
|
|
||||||
add_entities(entities)
|
add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class HikvisionData(object):
|
class HikvisionData(object):
|
||||||
"""Hikvision camera event stream object."""
|
"""Hikvision device event stream object."""
|
||||||
|
|
||||||
def __init__(self, hass, url, port, name, username, password):
|
def __init__(self, hass, url, port, name, username, password):
|
||||||
"""Initialize the data oject."""
|
"""Initialize the data oject."""
|
||||||
|
@ -144,25 +150,40 @@ class HikvisionData(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cam_id(self):
|
def cam_id(self):
|
||||||
"""Return camera id."""
|
"""Return device id."""
|
||||||
return self.camdata.get_id
|
return self.camdata.get_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return camera name."""
|
"""Return device name."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self):
|
||||||
|
"""Return device type."""
|
||||||
|
return self.camdata.get_type
|
||||||
|
|
||||||
|
def get_attributes(self, sensor, channel):
|
||||||
|
"""Return attribute list for sensor/channel."""
|
||||||
|
return self.camdata.fetch_attributes(sensor, channel)
|
||||||
|
|
||||||
|
|
||||||
class HikvisionBinarySensor(BinarySensorDevice):
|
class HikvisionBinarySensor(BinarySensorDevice):
|
||||||
"""Representation of a Hikvision binary sensor."""
|
"""Representation of a Hikvision binary sensor."""
|
||||||
|
|
||||||
def __init__(self, hass, sensor, cam, delay):
|
def __init__(self, hass, sensor, channel, cam, delay):
|
||||||
"""Initialize the binary_sensor."""
|
"""Initialize the binary_sensor."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._cam = cam
|
self._cam = cam
|
||||||
self._name = self._cam.name + ' ' + sensor
|
|
||||||
self._id = self._cam.cam_id + '.' + sensor
|
|
||||||
self._sensor = sensor
|
self._sensor = sensor
|
||||||
|
self._channel = channel
|
||||||
|
|
||||||
|
if self._cam.type == 'NVR':
|
||||||
|
self._name = '{} {} {}'.format(self._cam.name, sensor, channel)
|
||||||
|
else:
|
||||||
|
self._name = '{} {}'.format(self._cam.name, sensor)
|
||||||
|
|
||||||
|
self._id = '{}.{}.{}'.format(self._cam.cam_id, sensor, channel)
|
||||||
|
|
||||||
if delay is None:
|
if delay is None:
|
||||||
self._delay = 0
|
self._delay = 0
|
||||||
|
@ -176,11 +197,11 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||||
|
|
||||||
def _sensor_state(self):
|
def _sensor_state(self):
|
||||||
"""Extract sensor state."""
|
"""Extract sensor state."""
|
||||||
return self._cam.sensors[self._sensor][0]
|
return self._cam.get_attributes(self._sensor, self._channel)[0]
|
||||||
|
|
||||||
def _sensor_last_update(self):
|
def _sensor_last_update(self):
|
||||||
"""Extract sensor last update time."""
|
"""Extract sensor last update time."""
|
||||||
return self._cam.sensors[self._sensor][3]
|
return self._cam.get_attributes(self._sensor, self._channel)[3]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
@ -32,7 +32,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
InsteonPLMBinarySensorDevice(hass, plm, address, name)
|
InsteonPLMBinarySensorDevice(hass, plm, address, name)
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.async_add_job(async_add_devices(device_list))
|
async_add_devices(device_list)
|
||||||
|
|
||||||
|
|
||||||
class InsteonPLMBinarySensorDevice(BinarySensorDevice):
|
class InsteonPLMBinarySensorDevice(BinarySensorDevice):
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
"""
|
||||||
|
This component provides HA sensor support for Ring Door Bell/Chimes.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/binary_sensor.ring/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
from homeassistant.components.ring import (
|
||||||
|
CONF_ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE)
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ATTRIBUTION, CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS)
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import (
|
||||||
|
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['ring']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=5)
|
||||||
|
|
||||||
|
# Sensor types: Name, category, device_class
|
||||||
|
SENSOR_TYPES = {
|
||||||
|
'ding': ['Ding', ['doorbell'], 'occupancy'],
|
||||||
|
'motion': ['Motion', ['doorbell'], 'motion'],
|
||||||
|
}
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE):
|
||||||
|
cv.string,
|
||||||
|
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
|
||||||
|
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Set up a sensor for a Ring device."""
|
||||||
|
ring = hass.data.get('ring')
|
||||||
|
|
||||||
|
sensors = []
|
||||||
|
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
|
||||||
|
for device in ring.doorbells:
|
||||||
|
if 'doorbell' in SENSOR_TYPES[sensor_type][1]:
|
||||||
|
sensors.append(RingBinarySensor(hass,
|
||||||
|
device,
|
||||||
|
sensor_type))
|
||||||
|
add_devices(sensors, True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class RingBinarySensor(BinarySensorDevice):
|
||||||
|
"""A binary sensor implementation for Ring device."""
|
||||||
|
|
||||||
|
def __init__(self, hass, data, sensor_type):
|
||||||
|
"""Initialize a sensor for Ring device."""
|
||||||
|
super(RingBinarySensor, self).__init__()
|
||||||
|
self._sensor_type = sensor_type
|
||||||
|
self._data = data
|
||||||
|
self._name = "{0} {1}".format(self._data.name,
|
||||||
|
SENSOR_TYPES.get(self._sensor_type)[0])
|
||||||
|
self._device_class = SENSOR_TYPES.get(self._sensor_type)[2]
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return True if the binary sensor is on."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self):
|
||||||
|
"""Return the class of the binary sensor."""
|
||||||
|
return self._device_class
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
attrs = {}
|
||||||
|
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
|
||||||
|
|
||||||
|
attrs['device_id'] = self._data.id
|
||||||
|
attrs['firmware'] = self._data.firmware
|
||||||
|
attrs['timezone'] = self._data.timezone
|
||||||
|
|
||||||
|
if self._data.alert and self._data.alert_expires_at:
|
||||||
|
attrs['expires_at'] = self._data.alert_expires_at
|
||||||
|
attrs['state'] = self._data.alert.get('state')
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Get the latest data and updates the state."""
|
||||||
|
self._data.check_alerts()
|
||||||
|
|
||||||
|
if self._data.alert:
|
||||||
|
self._state = (self._sensor_type ==
|
||||||
|
self._data.alert.get('kind'))
|
||||||
|
else:
|
||||||
|
self._state = False
|
|
@ -1,4 +1,9 @@
|
||||||
"""Sensor to indicate whether the current day is a workday."""
|
"""
|
||||||
|
Sensor to indicate whether the current day is a workday.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/binary_sensor.workday/
|
||||||
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import datetime
|
import datetime
|
||||||
|
@ -6,8 +11,8 @@ import datetime
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN,
|
from homeassistant.const import (
|
||||||
CONF_NAME, WEEKDAYS)
|
STATE_ON, STATE_OFF, STATE_UNKNOWN, CONF_NAME, WEEKDAYS)
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
@ -48,29 +53,18 @@ 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 Workday sensor."""
|
"""Set up the Workday sensor."""
|
||||||
import holidays
|
import holidays
|
||||||
|
|
||||||
# Get the Sensor name from the config
|
|
||||||
sensor_name = config.get(CONF_NAME)
|
sensor_name = config.get(CONF_NAME)
|
||||||
|
|
||||||
# Get the country code from the config
|
|
||||||
country = config.get(CONF_COUNTRY)
|
country = config.get(CONF_COUNTRY)
|
||||||
|
|
||||||
# Get the province from the config
|
|
||||||
province = config.get(CONF_PROVINCE)
|
province = config.get(CONF_PROVINCE)
|
||||||
|
|
||||||
# Get the list of workdays from the config
|
|
||||||
workdays = config.get(CONF_WORKDAYS)
|
workdays = config.get(CONF_WORKDAYS)
|
||||||
|
|
||||||
# Get the list of excludes from the config
|
|
||||||
excludes = config.get(CONF_EXCLUDES)
|
excludes = config.get(CONF_EXCLUDES)
|
||||||
|
|
||||||
# Instantiate the holidays module for the current year
|
|
||||||
year = datetime.datetime.now().year
|
year = datetime.datetime.now().year
|
||||||
obj_holidays = getattr(holidays, country)(years=year)
|
obj_holidays = getattr(holidays, country)(years=year)
|
||||||
|
|
||||||
# Also apply the provience, if available for the configured country
|
|
||||||
if province:
|
if province:
|
||||||
if province not in obj_holidays.PROVINCES:
|
if province not in obj_holidays.PROVINCES:
|
||||||
_LOGGER.error('There is no province/state %s in country %s',
|
_LOGGER.error('There is no province/state %s in country %s',
|
||||||
|
@ -81,14 +75,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
obj_holidays = getattr(holidays, country)(prov=province,
|
obj_holidays = getattr(holidays, country)(prov=province,
|
||||||
years=year)
|
years=year)
|
||||||
|
|
||||||
# Output found public holidays via the debug channel
|
|
||||||
_LOGGER.debug("Found the following holidays for your configuration:")
|
_LOGGER.debug("Found the following holidays for your configuration:")
|
||||||
for date, name in sorted(obj_holidays.items()):
|
for date, name in sorted(obj_holidays.items()):
|
||||||
_LOGGER.debug("%s %s", date, name)
|
_LOGGER.debug("%s %s", date, name)
|
||||||
|
|
||||||
# Add ourselves as device
|
add_devices([IsWorkdaySensor(
|
||||||
add_devices([IsWorkdaySensor(obj_holidays, workdays,
|
obj_holidays, workdays, excludes, sensor_name)], True)
|
||||||
excludes, sensor_name)], True)
|
|
||||||
|
|
||||||
|
|
||||||
def day_to_string(day):
|
def day_to_string(day):
|
||||||
|
@ -122,7 +114,6 @@ class IsWorkdaySensor(Entity):
|
||||||
|
|
||||||
def is_include(self, day, now):
|
def is_include(self, day, now):
|
||||||
"""Check if given day is in the includes list."""
|
"""Check if given day is in the includes list."""
|
||||||
# Check includes
|
|
||||||
if day in self._workdays:
|
if day in self._workdays:
|
||||||
return True
|
return True
|
||||||
elif 'holiday' in self._workdays and now in self._obj_holidays:
|
elif 'holiday' in self._workdays and now in self._obj_holidays:
|
||||||
|
@ -132,7 +123,6 @@ class IsWorkdaySensor(Entity):
|
||||||
|
|
||||||
def is_exclude(self, day, now):
|
def is_exclude(self, day, now):
|
||||||
"""Check if given day is in the excludes list."""
|
"""Check if given day is in the excludes list."""
|
||||||
# Check excludes
|
|
||||||
if day in self._excludes:
|
if day in self._excludes:
|
||||||
return True
|
return True
|
||||||
elif 'holiday' in self._excludes and now in self._obj_holidays:
|
elif 'holiday' in self._excludes and now in self._obj_holidays:
|
||||||
|
|
|
@ -5,17 +5,19 @@ For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/blink/
|
https://home-assistant.io/components/blink/
|
||||||
"""
|
"""
|
||||||
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.const import (CONF_USERNAME,
|
from homeassistant.const import (
|
||||||
CONF_PASSWORD,
|
CONF_USERNAME, CONF_PASSWORD, ATTR_FRIENDLY_NAME, ATTR_ARMED)
|
||||||
ATTR_FRIENDLY_NAME,
|
|
||||||
ATTR_ARMED)
|
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
|
|
||||||
|
REQUIREMENTS = ['blinkpy==0.5.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'blink'
|
DOMAIN = 'blink'
|
||||||
REQUIREMENTS = ['blinkpy==0.5.2']
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
|
@ -50,7 +52,7 @@ class BlinkSystem(object):
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup Blink System."""
|
"""Set up Blink System."""
|
||||||
hass.data[DOMAIN] = BlinkSystem(config)
|
hass.data[DOMAIN] = BlinkSystem(config)
|
||||||
discovery.load_platform(hass, 'camera', DOMAIN, {}, config)
|
discovery.load_platform(hass, 'camera', DOMAIN, {}, config)
|
||||||
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
|
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
|
||||||
|
@ -77,11 +79,11 @@ def setup(hass, config):
|
||||||
hass.data[DOMAIN].blink.arm = value
|
hass.data[DOMAIN].blink.arm = value
|
||||||
hass.data[DOMAIN].blink.refresh()
|
hass.data[DOMAIN].blink.refresh()
|
||||||
|
|
||||||
hass.services.register(DOMAIN, 'snap_picture', snap_picture,
|
hass.services.register(
|
||||||
schema=SNAP_PICTURE_SCHEMA)
|
DOMAIN, 'snap_picture', snap_picture, schema=SNAP_PICTURE_SCHEMA)
|
||||||
hass.services.register(DOMAIN, 'arm_camera', arm_camera,
|
hass.services.register(
|
||||||
schema=ARM_CAMERA_SCHEMA)
|
DOMAIN, 'arm_camera', arm_camera, schema=ARM_CAMERA_SCHEMA)
|
||||||
hass.services.register(DOMAIN, 'arm_system', arm_system,
|
hass.services.register(
|
||||||
schema=ARM_SYSTEM_SCHEMA)
|
DOMAIN, 'arm_system', arm_system, schema=ARM_SYSTEM_SCHEMA)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -7,6 +7,7 @@ https://home-assistant.io/components/camera/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import collections
|
import collections
|
||||||
|
from contextlib import suppress
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
import hashlib
|
import hashlib
|
||||||
|
@ -58,7 +59,6 @@ def async_get_image(hass, entity_id, timeout=10):
|
||||||
state.attributes.get(ATTR_ENTITY_PICTURE)
|
state.attributes.get(ATTR_ENTITY_PICTURE)
|
||||||
)
|
)
|
||||||
|
|
||||||
response = None
|
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||||
response = yield from websession.get(url)
|
response = yield from websession.get(url)
|
||||||
|
@ -70,13 +70,9 @@ def async_get_image(hass, entity_id, timeout=10):
|
||||||
image = yield from response.read()
|
image = yield from response.read()
|
||||||
return image
|
return image
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
raise HomeAssistantError("Can't connect to {0}".format(url))
|
raise HomeAssistantError("Can't connect to {0}".format(url))
|
||||||
|
|
||||||
finally:
|
|
||||||
if response is not None:
|
|
||||||
yield from response.release()
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup(hass, config):
|
def async_setup(hass, config):
|
||||||
|
@ -172,7 +168,7 @@ class Camera(Entity):
|
||||||
if not img_bytes:
|
if not img_bytes:
|
||||||
break
|
break
|
||||||
|
|
||||||
if img_bytes is not None and img_bytes != last_image:
|
if img_bytes and img_bytes != last_image:
|
||||||
write(img_bytes)
|
write(img_bytes)
|
||||||
|
|
||||||
# Chrome seems to always ignore first picture,
|
# Chrome seems to always ignore first picture,
|
||||||
|
@ -185,8 +181,8 @@ class Camera(Entity):
|
||||||
|
|
||||||
yield from asyncio.sleep(.5)
|
yield from asyncio.sleep(.5)
|
||||||
|
|
||||||
except (asyncio.CancelledError, ConnectionResetError):
|
except asyncio.CancelledError:
|
||||||
_LOGGER.debug("Close stream by frontend.")
|
_LOGGER.debug("Stream closed by frontend.")
|
||||||
response = None
|
response = None
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
@ -268,16 +264,14 @@ class CameraImageView(CameraView):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def handle(self, request, camera):
|
def handle(self, request, camera):
|
||||||
"""Serve camera image."""
|
"""Serve camera image."""
|
||||||
try:
|
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
||||||
|
with async_timeout.timeout(10, loop=request.app['hass'].loop):
|
||||||
image = yield from camera.async_camera_image()
|
image = yield from camera.async_camera_image()
|
||||||
|
|
||||||
if image is None:
|
if image:
|
||||||
return web.Response(status=500)
|
|
||||||
|
|
||||||
return web.Response(body=image)
|
return web.Response(body=image)
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
return web.Response(status=500)
|
||||||
_LOGGER.debug("Close stream by frontend.")
|
|
||||||
|
|
||||||
|
|
||||||
class CameraMjpegStream(CameraView):
|
class CameraMjpegStream(CameraView):
|
||||||
|
|
|
@ -16,9 +16,9 @@ from homeassistant.const import (
|
||||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.aiohttp_client import (
|
from homeassistant.helpers.aiohttp_client import (
|
||||||
async_get_clientsession, async_aiohttp_proxy_stream)
|
async_get_clientsession, async_aiohttp_proxy_web)
|
||||||
|
|
||||||
REQUIREMENTS = ['amcrest==1.1.4']
|
REQUIREMENTS = ['amcrest==1.1.8']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ class AmcrestCam(Camera):
|
||||||
stream_coro = websession.get(
|
stream_coro = websession.get(
|
||||||
streaming_url, auth=self._token, timeout=TIMEOUT)
|
streaming_url, auth=self._token, timeout=TIMEOUT)
|
||||||
|
|
||||||
yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro)
|
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
@ -8,14 +8,14 @@ import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_NAME
|
||||||
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 (
|
||||||
DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
|
DATA_FFMPEG, 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.helpers.aiohttp_client import (
|
||||||
|
async_aiohttp_proxy_stream)
|
||||||
DEPENDENCIES = ['ffmpeg']
|
DEPENDENCIES = ['ffmpeg']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -69,26 +69,10 @@ class FFmpegCamera(Camera):
|
||||||
yield from stream.open_camera(
|
yield from stream.open_camera(
|
||||||
self._input, extra_cmd=self._extra_arguments)
|
self._input, extra_cmd=self._extra_arguments)
|
||||||
|
|
||||||
response = web.StreamResponse()
|
yield from async_aiohttp_proxy_stream(
|
||||||
response.content_type = 'multipart/x-mixed-replace;boundary=ffserver'
|
self.hass, request, stream,
|
||||||
|
'multipart/x-mixed-replace;boundary=ffserver')
|
||||||
yield from response.prepare(request)
|
|
||||||
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
data = yield from stream.read(102400)
|
|
||||||
if not data:
|
|
||||||
break
|
|
||||||
response.write(data)
|
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
_LOGGER.debug("Close stream by frontend.")
|
|
||||||
response = None
|
|
||||||
|
|
||||||
finally:
|
|
||||||
yield from stream.close()
|
yield from stream.close()
|
||||||
if response is not None:
|
|
||||||
yield from response.write_eof()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
@ -66,8 +66,12 @@ class FoscamCamera(Camera):
|
||||||
def camera_image(self):
|
def camera_image(self):
|
||||||
"""Return a still image reponse from the camera."""
|
"""Return a still image reponse from the camera."""
|
||||||
# Send the request to snap a picture and return raw jpg data
|
# Send the request to snap a picture and return raw jpg data
|
||||||
|
# Handle exception if host is not reachable or url failed
|
||||||
|
try:
|
||||||
response = requests.get(self._snap_picture_url, timeout=10)
|
response = requests.get(self._snap_picture_url, timeout=10)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
return response.content
|
return response.content
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -107,7 +107,6 @@ class GenericCamera(Camera):
|
||||||
None, fetch)
|
None, fetch)
|
||||||
# async
|
# async
|
||||||
else:
|
else:
|
||||||
response = None
|
|
||||||
try:
|
try:
|
||||||
websession = async_get_clientsession(self.hass)
|
websession = async_get_clientsession(self.hass)
|
||||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||||
|
@ -117,14 +116,9 @@ class GenericCamera(Camera):
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
_LOGGER.error('Timeout getting camera image')
|
_LOGGER.error('Timeout getting camera image')
|
||||||
return self._last_image
|
return self._last_image
|
||||||
except (aiohttp.errors.ClientError,
|
except aiohttp.ClientError as err:
|
||||||
aiohttp.errors.DisconnectedError,
|
|
||||||
aiohttp.errors.HttpProcessingError) as err:
|
|
||||||
_LOGGER.error('Error getting new camera image: %s', err)
|
_LOGGER.error('Error getting new camera image: %s', err)
|
||||||
return self._last_image
|
return self._last_image
|
||||||
finally:
|
|
||||||
if response is not None:
|
|
||||||
yield from response.release()
|
|
||||||
|
|
||||||
self._last_url = url
|
self._last_url = url
|
||||||
return self._last_image
|
return self._last_image
|
||||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
||||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||||
from homeassistant.helpers.aiohttp_client import (
|
from homeassistant.helpers.aiohttp_client import (
|
||||||
async_get_clientsession, async_aiohttp_proxy_stream)
|
async_get_clientsession, async_aiohttp_proxy_web)
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -93,7 +93,6 @@ class MjpegCamera(Camera):
|
||||||
return image
|
return image
|
||||||
|
|
||||||
websession = async_get_clientsession(self.hass)
|
websession = async_get_clientsession(self.hass)
|
||||||
response = None
|
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||||
response = yield from websession.get(
|
response = yield from websession.get(
|
||||||
|
@ -105,14 +104,9 @@ class MjpegCamera(Camera):
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
_LOGGER.error('Timeout getting camera image')
|
_LOGGER.error('Timeout getting camera image')
|
||||||
|
|
||||||
except (aiohttp.errors.ClientError,
|
except aiohttp.ClientError as err:
|
||||||
aiohttp.errors.ClientDisconnectedError) as err:
|
|
||||||
_LOGGER.error('Error getting new camera image: %s', err)
|
_LOGGER.error('Error getting new camera image: %s', err)
|
||||||
|
|
||||||
finally:
|
|
||||||
if response is not None:
|
|
||||||
yield from response.release()
|
|
||||||
|
|
||||||
def camera_image(self):
|
def camera_image(self):
|
||||||
"""Return a still image response from the camera."""
|
"""Return a still image response from the camera."""
|
||||||
if self._username and self._password:
|
if self._username and self._password:
|
||||||
|
@ -140,7 +134,7 @@ class MjpegCamera(Camera):
|
||||||
websession = async_get_clientsession(self.hass)
|
websession = async_get_clientsession(self.hass)
|
||||||
stream_coro = websession.get(self._mjpeg_url, auth=self._auth)
|
stream_coro = websession.get(self._mjpeg_url, auth=self._auth)
|
||||||
|
|
||||||
yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro)
|
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.components.camera import (
|
||||||
Camera, PLATFORM_SCHEMA)
|
Camera, PLATFORM_SCHEMA)
|
||||||
from homeassistant.helpers.aiohttp_client import (
|
from homeassistant.helpers.aiohttp_client import (
|
||||||
async_get_clientsession, async_create_clientsession,
|
async_get_clientsession, async_create_clientsession,
|
||||||
async_aiohttp_proxy_stream)
|
async_aiohttp_proxy_web)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util.async import run_coroutine_threadsafe
|
from homeassistant.util.async import run_coroutine_threadsafe
|
||||||
|
|
||||||
|
@ -74,7 +74,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
'version': '1',
|
'version': '1',
|
||||||
'query': 'SYNO.'
|
'query': 'SYNO.'
|
||||||
}
|
}
|
||||||
query_req = None
|
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||||
query_req = yield from websession_init.get(
|
query_req = yield from websession_init.get(
|
||||||
|
@ -88,14 +87,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
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']
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.exception("Error on %s", syno_api_url)
|
_LOGGER.exception("Error on %s", syno_api_url)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
finally:
|
|
||||||
if query_req is not None:
|
|
||||||
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(
|
syno_auth_url = SYNO_API_URL.format(
|
||||||
config.get(CONF_URL), WEBAPI_PATH, auth_path)
|
config.get(CONF_URL), WEBAPI_PATH, auth_path)
|
||||||
|
@ -128,13 +123,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
syno_camera_url,
|
syno_camera_url,
|
||||||
params=camera_payload
|
params=camera_payload
|
||||||
)
|
)
|
||||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.exception("Error on %s", syno_camera_url)
|
_LOGGER.exception("Error on %s", syno_camera_url)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
camera_resp = yield from camera_req.json()
|
camera_resp = yield from camera_req.json()
|
||||||
cameras = camera_resp['data']['cameras']
|
cameras = camera_resp['data']['cameras']
|
||||||
yield from camera_req.release()
|
|
||||||
|
|
||||||
# add cameras
|
# add cameras
|
||||||
devices = []
|
devices = []
|
||||||
|
@ -172,7 +166,6 @@ def get_session_id(hass, websession, username, password, login_url, timeout):
|
||||||
'session': 'SurveillanceStation',
|
'session': 'SurveillanceStation',
|
||||||
'format': 'sid'
|
'format': 'sid'
|
||||||
}
|
}
|
||||||
auth_req = None
|
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||||
auth_req = yield from websession.get(
|
auth_req = yield from websession.get(
|
||||||
|
@ -182,14 +175,10 @@ def get_session_id(hass, websession, username, password, login_url, timeout):
|
||||||
auth_resp = yield from auth_req.json()
|
auth_resp = yield from auth_req.json()
|
||||||
return auth_resp['data']['sid']
|
return auth_resp['data']['sid']
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.exception("Error on %s", login_url)
|
_LOGGER.exception("Error on %s", login_url)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
finally:
|
|
||||||
if auth_req is not None:
|
|
||||||
yield from auth_req.release()
|
|
||||||
|
|
||||||
|
|
||||||
class SynologyCamera(Camera):
|
class SynologyCamera(Camera):
|
||||||
"""An implementation of a Synology NAS based IP camera."""
|
"""An implementation of a Synology NAS based IP camera."""
|
||||||
|
@ -235,12 +224,11 @@ class SynologyCamera(Camera):
|
||||||
image_url,
|
image_url,
|
||||||
params=image_payload
|
params=image_payload
|
||||||
)
|
)
|
||||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.exception("Error on %s", image_url)
|
_LOGGER.error("Error fetching %s", image_url)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
image = yield from response.read()
|
image = yield from response.read()
|
||||||
yield from response.release()
|
|
||||||
|
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
@ -260,7 +248,7 @@ class SynologyCamera(Camera):
|
||||||
stream_coro = self._websession.get(
|
stream_coro = self._websession.get(
|
||||||
streaming_url, params=streaming_payload)
|
streaming_url, params=streaming_payload)
|
||||||
|
|
||||||
yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro)
|
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
@ -692,18 +692,16 @@ class ClimateDevice(Entity):
|
||||||
|
|
||||||
def _convert_for_display(self, temp):
|
def _convert_for_display(self, temp):
|
||||||
"""Convert temperature into preferred units for display purposes."""
|
"""Convert temperature into preferred units for display purposes."""
|
||||||
if (temp is None or not isinstance(temp, Number) or
|
if temp is None or not isinstance(temp, Number):
|
||||||
self.temperature_unit == self.unit_of_measurement):
|
|
||||||
return temp
|
return temp
|
||||||
|
if self.temperature_unit != self.unit_of_measurement:
|
||||||
value = convert_temperature(temp, self.temperature_unit,
|
temp = convert_temperature(temp, self.temperature_unit,
|
||||||
self.unit_of_measurement)
|
self.unit_of_measurement)
|
||||||
|
|
||||||
# Round in the units appropriate
|
# Round in the units appropriate
|
||||||
if self.precision == PRECISION_HALVES:
|
if self.precision == PRECISION_HALVES:
|
||||||
return round(value * 2) / 2.0
|
return round(temp * 2) / 2.0
|
||||||
elif self.precision == PRECISION_TENTHS:
|
elif self.precision == PRECISION_TENTHS:
|
||||||
return round(value, 1)
|
return round(temp, 1)
|
||||||
else:
|
else:
|
||||||
# PRECISION_WHOLE as a fall back
|
# PRECISION_WHOLE as a fall back
|
||||||
return round(value)
|
return round(temp)
|
||||||
|
|
|
@ -16,11 +16,15 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
STATE_MANUAL = "manual"
|
STATE_MANUAL = "manual"
|
||||||
STATE_BOOST = "boost"
|
STATE_BOOST = "boost"
|
||||||
|
STATE_COMFORT = "comfort"
|
||||||
|
STATE_LOWERING = "lowering"
|
||||||
|
|
||||||
HM_STATE_MAP = {
|
HM_STATE_MAP = {
|
||||||
"AUTO_MODE": STATE_AUTO,
|
"AUTO_MODE": STATE_AUTO,
|
||||||
"MANU_MODE": STATE_MANUAL,
|
"MANU_MODE": STATE_MANUAL,
|
||||||
"BOOST_MODE": STATE_BOOST,
|
"BOOST_MODE": STATE_BOOST,
|
||||||
|
"COMFORT_MODE": STATE_COMFORT,
|
||||||
|
"LOWERING_MODE": STATE_LOWERING
|
||||||
}
|
}
|
||||||
|
|
||||||
HM_TEMP_MAP = [
|
HM_TEMP_MAP = [
|
||||||
|
|
|
@ -1,40 +1,40 @@
|
||||||
"""tado component to create a climate device for each zone."""
|
"""
|
||||||
|
Tado component to create a climate device for each zone.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/climate.tado/
|
||||||
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import TEMP_CELSIUS
|
from homeassistant.const import TEMP_CELSIUS
|
||||||
|
from homeassistant.components.climate import ClimateDevice
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.const import ATTR_TEMPERATURE
|
||||||
ClimateDevice)
|
from homeassistant.components.tado import DATA_TADO
|
||||||
from homeassistant.const import (
|
|
||||||
ATTR_TEMPERATURE)
|
|
||||||
from homeassistant.components.tado import (
|
|
||||||
DATA_TADO)
|
|
||||||
|
|
||||||
CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Default mytado mode
|
|
||||||
CONST_MODE_OFF = "OFF" # Switch off heating in a zone
|
|
||||||
|
|
||||||
# When we change the temperature setting, we need an overlay mode
|
|
||||||
# wait until tado changes the mode automatic
|
|
||||||
CONST_OVERLAY_TADO_MODE = "TADO_MODE"
|
|
||||||
# the user has change the temperature or mode manually
|
|
||||||
CONST_OVERLAY_MANUAL = "MANUAL"
|
|
||||||
# the temperature will be reset after a timespan
|
|
||||||
CONST_OVERLAY_TIMER = "TIMER"
|
|
||||||
|
|
||||||
OPERATION_LIST = {
|
|
||||||
CONST_OVERLAY_MANUAL: "Manual",
|
|
||||||
CONST_OVERLAY_TIMER: "Timer",
|
|
||||||
CONST_OVERLAY_TADO_MODE: "Tado mode",
|
|
||||||
CONST_MODE_SMART_SCHEDULE: "Smart schedule",
|
|
||||||
CONST_MODE_OFF: "Off"}
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONST_MODE_SMART_SCHEDULE = 'SMART_SCHEDULE' # Default mytado mode
|
||||||
|
CONST_MODE_OFF = 'OFF' # Switch off heating in a zone
|
||||||
|
|
||||||
|
# When we change the temperature setting, we need an overlay mode
|
||||||
|
# wait until tado changes the mode automatic
|
||||||
|
CONST_OVERLAY_TADO_MODE = 'TADO_MODE'
|
||||||
|
# the user has change the temperature or mode manually
|
||||||
|
CONST_OVERLAY_MANUAL = 'MANUAL'
|
||||||
|
# the temperature will be reset after a timespan
|
||||||
|
CONST_OVERLAY_TIMER = 'TIMER'
|
||||||
|
|
||||||
|
OPERATION_LIST = {
|
||||||
|
CONST_OVERLAY_MANUAL: 'Manual',
|
||||||
|
CONST_OVERLAY_TIMER: 'Timer',
|
||||||
|
CONST_OVERLAY_TADO_MODE: 'Tado mode',
|
||||||
|
CONST_MODE_SMART_SCHEDULE: 'Smart schedule',
|
||||||
|
CONST_MODE_OFF: 'Off',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the climate platform."""
|
"""Set up the Tado climate platform."""
|
||||||
# get the PyTado object from the hub component
|
|
||||||
tado = hass.data[DATA_TADO]
|
tado = hass.data[DATA_TADO]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -45,10 +45,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
|
||||||
climate_devices = []
|
climate_devices = []
|
||||||
for zone in zones:
|
for zone in zones:
|
||||||
climate_devices.append(create_climate_device(tado, hass,
|
climate_devices.append(create_climate_device(
|
||||||
zone,
|
tado, hass, zone, zone['name'], zone['id']))
|
||||||
zone['name'],
|
|
||||||
zone['id']))
|
|
||||||
|
|
||||||
if len(climate_devices) > 0:
|
if len(climate_devices) > 0:
|
||||||
add_devices(climate_devices, True)
|
add_devices(climate_devices, True)
|
||||||
|
@ -58,13 +56,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
|
||||||
|
|
||||||
def create_climate_device(tado, hass, zone, name, zone_id):
|
def create_climate_device(tado, hass, zone, name, zone_id):
|
||||||
"""Create a climate device."""
|
"""Create a Tado climate device."""
|
||||||
capabilities = tado.get_capabilities(zone_id)
|
capabilities = tado.get_capabilities(zone_id)
|
||||||
|
|
||||||
unit = TEMP_CELSIUS
|
unit = TEMP_CELSIUS
|
||||||
min_temp = float(capabilities["temperatures"]["celsius"]["min"])
|
min_temp = float(capabilities['temperatures']['celsius']['min'])
|
||||||
max_temp = float(capabilities["temperatures"]["celsius"]["max"])
|
max_temp = float(capabilities['temperatures']['celsius']['max'])
|
||||||
ac_mode = capabilities["type"] != "HEATING"
|
ac_mode = capabilities['type'] != 'HEATING'
|
||||||
|
|
||||||
data_id = 'zone {} {}'.format(name, zone_id)
|
data_id = 'zone {} {}'.format(name, zone_id)
|
||||||
device = TadoClimate(tado,
|
device = TadoClimate(tado,
|
||||||
|
@ -74,10 +72,10 @@ def create_climate_device(tado, hass, zone, name, zone_id):
|
||||||
ac_mode)
|
ac_mode)
|
||||||
|
|
||||||
tado.add_sensor(data_id, {
|
tado.add_sensor(data_id, {
|
||||||
"id": zone_id,
|
'id': zone_id,
|
||||||
"zone": zone,
|
'zone': zone,
|
||||||
"name": name,
|
'name': name,
|
||||||
"climate": device
|
'climate': device
|
||||||
})
|
})
|
||||||
|
|
||||||
return device
|
return device
|
||||||
|
@ -89,7 +87,7 @@ class TadoClimate(ClimateDevice):
|
||||||
def __init__(self, store, zone_name, zone_id, data_id,
|
def __init__(self, store, zone_name, zone_id, data_id,
|
||||||
min_temp, max_temp, ac_mode,
|
min_temp, max_temp, ac_mode,
|
||||||
tolerance=0.3):
|
tolerance=0.3):
|
||||||
"""Initialization of TadoClimate device."""
|
"""Initialization of Tado climate device."""
|
||||||
self._store = store
|
self._store = store
|
||||||
self._data_id = data_id
|
self._data_id = data_id
|
||||||
|
|
||||||
|
@ -202,8 +200,7 @@ class TadoClimate(ClimateDevice):
|
||||||
data = self._store.get_data(self._data_id)
|
data = self._store.get_data(self._data_id)
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
_LOGGER.debug('Recieved no data for zone %s',
|
_LOGGER.debug("Recieved no data for zone %s", self.zone_name)
|
||||||
self.zone_name)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if 'sensorDataPoints' in data:
|
if 'sensorDataPoints' in data:
|
||||||
|
@ -232,11 +229,11 @@ class TadoClimate(ClimateDevice):
|
||||||
|
|
||||||
if 'tadoMode' in data:
|
if 'tadoMode' in data:
|
||||||
mode = data['tadoMode']
|
mode = data['tadoMode']
|
||||||
self._is_away = mode == "AWAY"
|
self._is_away = mode == 'AWAY'
|
||||||
|
|
||||||
if 'setting' in data:
|
if 'setting' in data:
|
||||||
power = data['setting']['power']
|
power = data['setting']['power']
|
||||||
if power == "OFF":
|
if power == 'OFF':
|
||||||
self._current_operation = CONST_MODE_OFF
|
self._current_operation = CONST_MODE_OFF
|
||||||
self._device_is_active = False
|
self._device_is_active = False
|
||||||
else:
|
else:
|
||||||
|
@ -249,48 +246,47 @@ class TadoClimate(ClimateDevice):
|
||||||
overlay = False
|
overlay = False
|
||||||
termination = ""
|
termination = ""
|
||||||
|
|
||||||
# if you set mode manualy to off, there will be an overlay
|
# If you set mode manualy to off, there will be an overlay
|
||||||
# and a termination, but we want to see the mode "OFF"
|
# and a termination, but we want to see the mode "OFF"
|
||||||
|
|
||||||
if overlay and self._device_is_active:
|
if overlay and self._device_is_active:
|
||||||
# there is an overlay the device is on
|
# There is an overlay the device is on
|
||||||
self._overlay_mode = termination
|
self._overlay_mode = termination
|
||||||
self._current_operation = termination
|
self._current_operation = termination
|
||||||
else:
|
else:
|
||||||
# there is no overlay, the mode will always be
|
# There is no overlay, the mode will always be
|
||||||
# "SMART_SCHEDULE"
|
# "SMART_SCHEDULE"
|
||||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||||
|
|
||||||
def _control_heating(self):
|
def _control_heating(self):
|
||||||
"""Send new target temperature to mytado."""
|
"""Send new target temperature to mytado."""
|
||||||
if not self._active and None not in (self._cur_temp,
|
if not self._active and None not in (
|
||||||
self._target_temp):
|
self._cur_temp, self._target_temp):
|
||||||
self._active = True
|
self._active = True
|
||||||
_LOGGER.info('Obtained current and target temperature. '
|
_LOGGER.info("Obtained current and target temperature. "
|
||||||
'tado thermostat active.')
|
"Tado thermostat active")
|
||||||
|
|
||||||
if not self._active or self._current_operation == self._overlay_mode:
|
if not self._active or self._current_operation == self._overlay_mode:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._current_operation == CONST_MODE_SMART_SCHEDULE:
|
if self._current_operation == CONST_MODE_SMART_SCHEDULE:
|
||||||
_LOGGER.info('Switching mytado.com to SCHEDULE (default) '
|
_LOGGER.info("Switching mytado.com to SCHEDULE (default) "
|
||||||
'for zone %s', self.zone_name)
|
"for zone %s", self.zone_name)
|
||||||
self._store.reset_zone_overlay(self.zone_id)
|
self._store.reset_zone_overlay(self.zone_id)
|
||||||
self._overlay_mode = self._current_operation
|
self._overlay_mode = self._current_operation
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._current_operation == CONST_MODE_OFF:
|
if self._current_operation == CONST_MODE_OFF:
|
||||||
_LOGGER.info('Switching mytado.com to OFF for zone %s',
|
_LOGGER.info("Switching mytado.com to OFF for zone %s",
|
||||||
self.zone_name)
|
self.zone_name)
|
||||||
self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL)
|
self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL)
|
||||||
self._overlay_mode = self._current_operation
|
self._overlay_mode = self._current_operation
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.info('Switching mytado.com to %s mode for zone %s',
|
_LOGGER.info("Switching mytado.com to %s mode for zone %s",
|
||||||
self._current_operation, self.zone_name)
|
self._current_operation, self.zone_name)
|
||||||
self._store.set_zone_overlay(self.zone_id,
|
self._store.set_zone_overlay(
|
||||||
self._current_operation,
|
self.zone_id, self._current_operation, self._target_temp)
|
||||||
self._target_temp)
|
|
||||||
|
|
||||||
self._overlay_mode = self._current_operation
|
self._overlay_mode = self._current_operation
|
||||||
|
|
|
@ -19,7 +19,6 @@ DEPENDENCIES = ['wink']
|
||||||
STATE_AUX = 'aux'
|
STATE_AUX = 'aux'
|
||||||
STATE_ECO = 'eco'
|
STATE_ECO = 'eco'
|
||||||
STATE_FAN = 'fan'
|
STATE_FAN = 'fan'
|
||||||
SPEED_LOWEST = 'lowest'
|
|
||||||
SPEED_LOW = 'low'
|
SPEED_LOW = 'low'
|
||||||
SPEED_MEDIUM = 'medium'
|
SPEED_MEDIUM = 'medium'
|
||||||
SPEED_HIGH = 'high'
|
SPEED_HIGH = 'high'
|
||||||
|
@ -400,7 +399,7 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||||
op_list.append(STATE_COOL)
|
op_list.append(STATE_COOL)
|
||||||
if 'auto_eco' in modes:
|
if 'auto_eco' in modes:
|
||||||
op_list.append(STATE_ECO)
|
op_list.append(STATE_ECO)
|
||||||
if 'fan_eco' in modes:
|
if 'fan_only' in modes:
|
||||||
op_list.append(STATE_FAN)
|
op_list.append(STATE_FAN)
|
||||||
return op_list
|
return op_list
|
||||||
|
|
||||||
|
@ -439,9 +438,7 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||||
def current_fan_mode(self):
|
def current_fan_mode(self):
|
||||||
"""Return the current fan mode."""
|
"""Return the current fan mode."""
|
||||||
speed = self.wink.current_fan_speed()
|
speed = self.wink.current_fan_speed()
|
||||||
if speed <= 0.3 and speed >= 0.0:
|
if speed <= 0.4 and speed > 0.3:
|
||||||
return SPEED_LOWEST
|
|
||||||
elif speed <= 0.5 and speed > 0.3:
|
|
||||||
return SPEED_LOW
|
return SPEED_LOW
|
||||||
elif speed <= 0.8 and speed > 0.5:
|
elif speed <= 0.8 and speed > 0.5:
|
||||||
return SPEED_MEDIUM
|
return SPEED_MEDIUM
|
||||||
|
@ -453,14 +450,12 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||||
@property
|
@property
|
||||||
def fan_list(self):
|
def fan_list(self):
|
||||||
"""List of available fan modes."""
|
"""List of available fan modes."""
|
||||||
return [SPEED_LOWEST, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||||
|
|
||||||
def set_fan_mode(self, mode):
|
def set_fan_mode(self, mode):
|
||||||
"""Set fan speed."""
|
"""Set fan speed."""
|
||||||
if mode == SPEED_LOWEST:
|
if mode == SPEED_LOW:
|
||||||
speed = 0.3
|
speed = 0.4
|
||||||
elif mode == SPEED_LOW:
|
|
||||||
speed = 0.5
|
|
||||||
elif mode == SPEED_MEDIUM:
|
elif mode == SPEED_MEDIUM:
|
||||||
speed = 0.8
|
speed = 0.8
|
||||||
elif mode == SPEED_HIGH:
|
elif mode == SPEED_HIGH:
|
||||||
|
|
|
@ -112,6 +112,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||||
"current_temperature=%s",
|
"current_temperature=%s",
|
||||||
self._current_temperature)
|
self._current_temperature)
|
||||||
|
if self._current_temperature is not None:
|
||||||
self._target_temperature = (
|
self._target_temperature = (
|
||||||
round((float(self._current_temperature)), 1))
|
round((float(self._current_temperature)), 1))
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -47,6 +47,7 @@ class VeraCover(VeraDevice, CoverDevice):
|
||||||
def set_cover_position(self, position, **kwargs):
|
def set_cover_position(self, position, **kwargs):
|
||||||
"""Move the cover to a specific position."""
|
"""Move the cover to a specific position."""
|
||||||
self.vera_device.set_level(position)
|
self.vera_device.set_level(position)
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_closed(self):
|
def is_closed(self):
|
||||||
|
@ -60,11 +61,14 @@ class VeraCover(VeraDevice, CoverDevice):
|
||||||
def open_cover(self, **kwargs):
|
def open_cover(self, **kwargs):
|
||||||
"""Open the cover."""
|
"""Open the cover."""
|
||||||
self.vera_device.open()
|
self.vera_device.open()
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def close_cover(self, **kwargs):
|
def close_cover(self, **kwargs):
|
||||||
"""Close the cover."""
|
"""Close the cover."""
|
||||||
self.vera_device.close()
|
self.vera_device.close()
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def stop_cover(self, **kwargs):
|
def stop_cover(self, **kwargs):
|
||||||
"""Stop the cover."""
|
"""Stop the cover."""
|
||||||
self.vera_device.stop()
|
self.vera_device.stop()
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
|
@ -20,12 +20,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||||
|
|
||||||
|
|
||||||
def get_device(values, **kwargs):
|
def get_device(values, node_config, **kwargs):
|
||||||
"""Create zwave entity device."""
|
"""Create zwave entity device."""
|
||||||
|
invert_buttons = node_config.get(zwave.CONF_INVERT_OPENCLOSE_BUTTONS)
|
||||||
if (values.primary.command_class ==
|
if (values.primary.command_class ==
|
||||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||||
and values.primary.index == 0):
|
and values.primary.index == 0):
|
||||||
return ZwaveRollershutter(values)
|
return ZwaveRollershutter(values, invert_buttons)
|
||||||
elif (values.primary.command_class in [
|
elif (values.primary.command_class in [
|
||||||
zwave.const.COMMAND_CLASS_SWITCH_BINARY,
|
zwave.const.COMMAND_CLASS_SWITCH_BINARY,
|
||||||
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]):
|
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]):
|
||||||
|
@ -36,13 +37,14 @@ def get_device(values, **kwargs):
|
||||||
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||||
"""Representation of an Zwave roller shutter."""
|
"""Representation of an Zwave roller shutter."""
|
||||||
|
|
||||||
def __init__(self, values):
|
def __init__(self, values, invert_buttons):
|
||||||
"""Initialize the zwave rollershutter."""
|
"""Initialize the zwave rollershutter."""
|
||||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
self._open_id = None
|
self._open_id = None
|
||||||
self._close_id = None
|
self._close_id = None
|
||||||
self._current_position = None
|
self._current_position = None
|
||||||
|
self._invert_buttons = invert_buttons
|
||||||
|
|
||||||
self._workaround = workaround.get_device_mapping(values.primary)
|
self._workaround = workaround.get_device_mapping(values.primary)
|
||||||
if self._workaround:
|
if self._workaround:
|
||||||
|
@ -56,10 +58,9 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||||
|
|
||||||
if self.values.open and self.values.close and \
|
if self.values.open and self.values.close and \
|
||||||
self._open_id is None and self._close_id is None:
|
self._open_id is None and self._close_id is None:
|
||||||
if self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE:
|
if self._invert_buttons:
|
||||||
self._open_id = self.values.close.value_id
|
self._open_id = self.values.close.value_id
|
||||||
self._close_id = self.values.open.value_id
|
self._close_id = self.values.open.value_id
|
||||||
self._workaround = None
|
|
||||||
else:
|
else:
|
||||||
self._open_id = self.values.open.value_id
|
self._open_id = self.values.open.value_id
|
||||||
self._close_id = self.values.close.value_id
|
self._close_id = self.values.close.value_id
|
||||||
|
|
|
@ -553,7 +553,6 @@ class Device(Entity):
|
||||||
# bytes like 00 get truncates to 0, API needs full bytes
|
# bytes like 00 get truncates to 0, API needs full bytes
|
||||||
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
|
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
|
||||||
url = 'http://api.macvendors.com/' + oui
|
url = 'http://api.macvendors.com/' + oui
|
||||||
resp = None
|
|
||||||
try:
|
try:
|
||||||
websession = async_get_clientsession(self.hass)
|
websession = async_get_clientsession(self.hass)
|
||||||
|
|
||||||
|
@ -570,13 +569,9 @@ class Device(Entity):
|
||||||
# in the 'known_devices.yaml' file which only happens
|
# in the 'known_devices.yaml' file which only happens
|
||||||
# the first time the device is seen.
|
# the first time the device is seen.
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError,
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
aiohttp.errors.ClientDisconnectedError):
|
|
||||||
# same as above
|
# same as above
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
finally:
|
|
||||||
if resp is not None:
|
|
||||||
yield from resp.release()
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
|
|
|
@ -18,6 +18,7 @@ from homeassistant.components.device_tracker import ( # NOQA
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
|
URL = '/api/locative'
|
||||||
|
|
||||||
|
|
||||||
def setup_scanner(hass, config, see, discovery_info=None):
|
def setup_scanner(hass, config, see, discovery_info=None):
|
||||||
|
@ -30,7 +31,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||||
class LocativeView(HomeAssistantView):
|
class LocativeView(HomeAssistantView):
|
||||||
"""View to handle locative requests."""
|
"""View to handle locative requests."""
|
||||||
|
|
||||||
url = '/api/locative'
|
url = URL
|
||||||
name = 'api:locative'
|
name = 'api:locative'
|
||||||
|
|
||||||
def __init__(self, see):
|
def __init__(self, see):
|
||||||
|
|
|
@ -16,11 +16,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
def setup_scanner(hass, config, see, discovery_info=None):
|
def setup_scanner(hass, config, see, discovery_info=None):
|
||||||
"""Setup the MySensors tracker."""
|
"""Setup the MySensors tracker."""
|
||||||
def mysensors_callback(gateway, node_id):
|
def mysensors_callback(gateway, msg):
|
||||||
"""Callback for mysensors platform."""
|
"""Callback for mysensors platform."""
|
||||||
node = gateway.sensors[node_id]
|
node = gateway.sensors[msg.node_id]
|
||||||
if node.sketch_name is None:
|
if node.sketch_name is None:
|
||||||
_LOGGER.info('No sketch_name: node %s', node_id)
|
_LOGGER.info('No sketch_name: node %s', msg.node_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
pres = gateway.const.Presentation
|
pres = gateway.const.Presentation
|
||||||
|
@ -37,12 +37,12 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||||
'latitude,longitude,altitude', position)
|
'latitude,longitude,altitude', position)
|
||||||
continue
|
continue
|
||||||
name = '{} {} {}'.format(
|
name = '{} {} {}'.format(
|
||||||
node.sketch_name, node_id, child.id)
|
node.sketch_name, msg.node_id, child.id)
|
||||||
attr = {
|
attr = {
|
||||||
mysensors.ATTR_CHILD_ID: child.id,
|
mysensors.ATTR_CHILD_ID: child.id,
|
||||||
mysensors.ATTR_DESCRIPTION: child.description,
|
mysensors.ATTR_DESCRIPTION: child.description,
|
||||||
mysensors.ATTR_DEVICE: gateway.device,
|
mysensors.ATTR_DEVICE: gateway.device,
|
||||||
mysensors.ATTR_NODE_ID: node_id,
|
mysensors.ATTR_NODE_ID: msg.node_id,
|
||||||
}
|
}
|
||||||
see(
|
see(
|
||||||
dev_id=slugify(name),
|
dev_id=slugify(name),
|
||||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.util import Throttle
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['pysnmp==4.3.4']
|
REQUIREMENTS = ['pysnmp==4.3.5']
|
||||||
|
|
||||||
CONF_COMMUNITY = 'community'
|
CONF_COMMUNITY = 'community'
|
||||||
CONF_AUTHKEY = 'authkey'
|
CONF_AUTHKEY = 'authkey'
|
||||||
|
@ -125,7 +125,10 @@ class SnmpScanner(DeviceScanner):
|
||||||
|
|
||||||
for resrow in restable:
|
for resrow in restable:
|
||||||
for _, val in resrow:
|
for _, val in resrow:
|
||||||
|
try:
|
||||||
mac = binascii.hexlify(val.asOctets()).decode('utf-8')
|
mac = binascii.hexlify(val.asOctets()).decode('utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
_LOGGER.debug("Found MAC %s", mac)
|
_LOGGER.debug("Found MAC %s", mac)
|
||||||
mac = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)])
|
mac = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)])
|
||||||
devices.append({'mac': mac})
|
devices.append({'mac': mac})
|
||||||
|
|
|
@ -105,8 +105,6 @@ class TadoDeviceScanner(DeviceScanner):
|
||||||
_LOGGER.debug("Requesting Tado")
|
_LOGGER.debug("Requesting Tado")
|
||||||
|
|
||||||
last_results = []
|
last_results = []
|
||||||
response = None
|
|
||||||
tado_json = None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||||
|
@ -127,14 +125,10 @@ class TadoDeviceScanner(DeviceScanner):
|
||||||
|
|
||||||
tado_json = yield from response.json()
|
tado_json = yield from response.json()
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.error("Cannot load Tado data")
|
_LOGGER.error("Cannot load Tado data")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
finally:
|
|
||||||
if response is not None:
|
|
||||||
yield from response.release()
|
|
||||||
|
|
||||||
# Without a home_id, we fetched an URL where the mobile devices can be
|
# Without a home_id, we fetched an URL where the mobile devices can be
|
||||||
# found under the mobileDevices key.
|
# found under the mobileDevices key.
|
||||||
if 'mobileDevices' in tado_json:
|
if 'mobileDevices' in tado_json:
|
||||||
|
|
|
@ -101,7 +101,6 @@ class UPCDeviceScanner(DeviceScanner):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_login(self):
|
def async_login(self):
|
||||||
"""Login into firmware and get first token."""
|
"""Login into firmware and get first token."""
|
||||||
response = None
|
|
||||||
try:
|
try:
|
||||||
# get first token
|
# get first token
|
||||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||||
|
@ -110,6 +109,7 @@ class UPCDeviceScanner(DeviceScanner):
|
||||||
)
|
)
|
||||||
|
|
||||||
yield from response.text()
|
yield from response.text()
|
||||||
|
|
||||||
self.token = response.cookies['sessionToken'].value
|
self.token = response.cookies['sessionToken'].value
|
||||||
|
|
||||||
# login
|
# login
|
||||||
|
@ -119,18 +119,12 @@ class UPCDeviceScanner(DeviceScanner):
|
||||||
})
|
})
|
||||||
|
|
||||||
# successfull?
|
# successfull?
|
||||||
if data is not None:
|
return data is not None
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.error("Can not load login page from %s", self.host)
|
_LOGGER.error("Can not load login page from %s", self.host)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
finally:
|
|
||||||
if response is not None:
|
|
||||||
yield from response.release()
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def _async_ws_function(self, function, additional_form=None):
|
def _async_ws_function(self, function, additional_form=None):
|
||||||
"""Execute a command on UPC firmware webservice."""
|
"""Execute a command on UPC firmware webservice."""
|
||||||
|
@ -142,8 +136,7 @@ class UPCDeviceScanner(DeviceScanner):
|
||||||
if additional_form:
|
if additional_form:
|
||||||
form_data.update(additional_form)
|
form_data.update(additional_form)
|
||||||
|
|
||||||
redirects = True if function != CMD_DEVICES else False
|
redirects = function != CMD_DEVICES
|
||||||
response = None
|
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||||
response = yield from self.websession.post(
|
response = yield from self.websession.post(
|
||||||
|
@ -163,10 +156,6 @@ class UPCDeviceScanner(DeviceScanner):
|
||||||
self.token = response.cookies['sessionToken'].value
|
self.token = response.cookies['sessionToken'].value
|
||||||
return (yield from response.text())
|
return (yield from response.text())
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.error("Error on %s", function)
|
_LOGGER.error("Error on %s", function)
|
||||||
self.token = None
|
self.token = None
|
||||||
|
|
||||||
finally:
|
|
||||||
if response is not None:
|
|
||||||
yield from response.release()
|
|
||||||
|
|
|
@ -153,7 +153,7 @@ def setup(hass, config):
|
||||||
sw_path = "service_worker.js"
|
sw_path = "service_worker.js"
|
||||||
|
|
||||||
hass.http.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), False)
|
||||||
hass.http.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.http.register_static_path("/static", STATIC_PATH)
|
hass.http.register_static_path("/static", STATIC_PATH)
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
FINGERPRINTS = {
|
FINGERPRINTS = {
|
||||||
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
|
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
|
||||||
"core.js": "5d08475f03adb5969bd31855d5ca0cfd",
|
"core.js": "5d08475f03adb5969bd31855d5ca0cfd",
|
||||||
"frontend.html": "53c45b837a3bcae7cfb9ef4a5919844f",
|
"frontend.html": "feaf3e9453eca239f29eb10e7645a84f",
|
||||||
"mdi.html": "4921d26f29dc148c3e8bd5bcd8ce5822",
|
"mdi.html": "989f02c51eba561dc32b9ecc628a84b3",
|
||||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||||
"panels/ha-panel-config.html": "6dcb246cd356307a638f81c4f89bf9b3",
|
"panels/ha-panel-config.html": "6dcb246cd356307a638f81c4f89bf9b3",
|
||||||
"panels/ha-panel-dev-event.html": "1f169700c2345785855b1d7919d12326",
|
"panels/ha-panel-dev-event.html": "1f169700c2345785855b1d7919d12326",
|
||||||
|
@ -12,9 +12,9 @@ FINGERPRINTS = {
|
||||||
"panels/ha-panel-dev-service.html": "0fe8e6acdccf2dc3d1ae657b2c7f2df0",
|
"panels/ha-panel-dev-service.html": "0fe8e6acdccf2dc3d1ae657b2c7f2df0",
|
||||||
"panels/ha-panel-dev-state.html": "48d37db4a1d6708314ded1d624d0f4d4",
|
"panels/ha-panel-dev-state.html": "48d37db4a1d6708314ded1d624d0f4d4",
|
||||||
"panels/ha-panel-dev-template.html": "6f353392d68574fbc5af188bca44d0ae",
|
"panels/ha-panel-dev-template.html": "6f353392d68574fbc5af188bca44d0ae",
|
||||||
"panels/ha-panel-history.html": "bfd5f929d5aa9cefdd799ec37624efa1",
|
"panels/ha-panel-history.html": "6945cebe5d8075aae560d2c4246b063f",
|
||||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||||
"panels/ha-panel-logbook.html": "a1fc2b5d739bedb9d87e4da4cd929a71",
|
"panels/ha-panel-logbook.html": "a1fc2b5d739bedb9d87e4da4cd929a71",
|
||||||
"panels/ha-panel-map.html": "9aa065b1908089f3bb5af7fdf9485be5",
|
"panels/ha-panel-map.html": "e3c7a54f90dd4269d7e53cdcd96514c6",
|
||||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1 +1 @@
|
||||||
Subproject commit f4c59e1eff3223262c198a29cf70c62572de019b
|
Subproject commit abbdc6f055524c5d3ed0bb50e35400fed40d573f
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -0,0 +1,272 @@
|
||||||
|
"""
|
||||||
|
Exposes regular rest commands as services.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/hassio/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from aiohttp import web
|
||||||
|
from aiohttp.web_exceptions import HTTPBadGateway
|
||||||
|
import async_timeout
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config import load_yaml_config_file
|
||||||
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
DOMAIN = 'hassio'
|
||||||
|
DEPENDENCIES = ['http']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
LONG_TASK_TIMEOUT = 900
|
||||||
|
DEFAULT_TIMEOUT = 10
|
||||||
|
|
||||||
|
SERVICE_HOST_SHUTDOWN = 'host_shutdown'
|
||||||
|
SERVICE_HOST_REBOOT = 'host_reboot'
|
||||||
|
|
||||||
|
SERVICE_HOST_UPDATE = 'host_update'
|
||||||
|
SERVICE_SUPERVISOR_UPDATE = 'supervisor_update'
|
||||||
|
SERVICE_HOMEASSISTANT_UPDATE = 'homeassistant_update'
|
||||||
|
|
||||||
|
SERVICE_ADDON_INSTALL = 'addon_install'
|
||||||
|
SERVICE_ADDON_UNINSTALL = 'addon_uninstall'
|
||||||
|
SERVICE_ADDON_UPDATE = 'addon_update'
|
||||||
|
SERVICE_ADDON_START = 'addon_start'
|
||||||
|
SERVICE_ADDON_STOP = 'addon_stop'
|
||||||
|
|
||||||
|
ATTR_ADDON = 'addon'
|
||||||
|
ATTR_VERSION = 'version'
|
||||||
|
|
||||||
|
|
||||||
|
SCHEMA_SERVICE_UPDATE = vol.Schema({
|
||||||
|
vol.Optional(ATTR_VERSION): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
SCHEMA_SERVICE_ADDONS = vol.Schema({
|
||||||
|
vol.Required(ATTR_ADDON): cv.slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
SCHEMA_SERVICE_ADDONS_VERSION = SCHEMA_SERVICE_ADDONS.extend({
|
||||||
|
vol.Optional(ATTR_VERSION): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
SERVICE_MAP = {
|
||||||
|
SERVICE_HOST_SHUTDOWN: None,
|
||||||
|
SERVICE_HOST_REBOOT: None,
|
||||||
|
SERVICE_HOST_UPDATE: SCHEMA_SERVICE_UPDATE,
|
||||||
|
SERVICE_SUPERVISOR_UPDATE: SCHEMA_SERVICE_UPDATE,
|
||||||
|
SERVICE_HOMEASSISTANT_UPDATE: SCHEMA_SERVICE_UPDATE,
|
||||||
|
SERVICE_ADDON_INSTALL: SCHEMA_SERVICE_ADDONS_VERSION,
|
||||||
|
SERVICE_ADDON_UNINSTALL: SCHEMA_SERVICE_ADDONS,
|
||||||
|
SERVICE_ADDON_START: SCHEMA_SERVICE_ADDONS,
|
||||||
|
SERVICE_ADDON_STOP: SCHEMA_SERVICE_ADDONS,
|
||||||
|
SERVICE_ADDON_UPDATE: SCHEMA_SERVICE_ADDONS_VERSION,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup(hass, config):
|
||||||
|
"""Setup the hassio component."""
|
||||||
|
try:
|
||||||
|
host = os.environ['HASSIO']
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.error("No HassIO supervisor detect!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
websession = async_get_clientsession(hass)
|
||||||
|
hassio = HassIO(hass.loop, websession, host)
|
||||||
|
|
||||||
|
api_ok = yield from hassio.is_connected()
|
||||||
|
if not api_ok:
|
||||||
|
_LOGGER.error("Not connected with HassIO!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# register base api views
|
||||||
|
for base in ('host', 'homeassistant'):
|
||||||
|
hass.http.register_view(HassIOBaseView(hassio, base))
|
||||||
|
for base in ('supervisor', 'network'):
|
||||||
|
hass.http.register_view(HassIOBaseEditView(hassio, base))
|
||||||
|
|
||||||
|
# register view for addons
|
||||||
|
hass.http.register_view(HassIOAddonsView(hassio))
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_service_handler(service):
|
||||||
|
"""Handle HassIO service calls."""
|
||||||
|
addon = service.data.get(ATTR_ADDON)
|
||||||
|
if ATTR_VERSION in service.data:
|
||||||
|
version = {ATTR_VERSION: service.data[ATTR_VERSION]}
|
||||||
|
else:
|
||||||
|
version = None
|
||||||
|
|
||||||
|
# map to api call
|
||||||
|
if service.service == SERVICE_HOST_UPDATE:
|
||||||
|
yield from hassio.send_command(
|
||||||
|
"/host/update", payload=version)
|
||||||
|
elif service.service == SERVICE_HOST_REBOOT:
|
||||||
|
yield from hassio.send_command("/host/reboot")
|
||||||
|
elif service.service == SERVICE_HOST_SHUTDOWN:
|
||||||
|
yield from hassio.send_command("/host/shutdown")
|
||||||
|
elif service.service == SERVICE_SUPERVISOR_UPDATE:
|
||||||
|
yield from hassio.send_command(
|
||||||
|
"/supervisor/update", payload=version)
|
||||||
|
elif service.service == SERVICE_HOMEASSISTANT_UPDATE:
|
||||||
|
yield from hassio.send_command(
|
||||||
|
"/homeassistant/update", payload=version,
|
||||||
|
timeout=LONG_TASK_TIMEOUT)
|
||||||
|
elif service.service == SERVICE_ADDON_INSTALL:
|
||||||
|
yield from hassio.send_command(
|
||||||
|
"/addons/{}/install".format(addon), payload=version,
|
||||||
|
timeout=LONG_TASK_TIMEOUT)
|
||||||
|
elif service.service == SERVICE_ADDON_UNINSTALL:
|
||||||
|
yield from hassio.send_command(
|
||||||
|
"/addons/{}/uninstall".format(addon))
|
||||||
|
elif service.service == SERVICE_ADDON_START:
|
||||||
|
yield from hassio.send_command("/addons/{}/start".format(addon))
|
||||||
|
elif service.service == SERVICE_ADDON_STOP:
|
||||||
|
yield from hassio.send_command("/addons/{}/stop".format(addon))
|
||||||
|
elif service.service == SERVICE_ADDON_UPDATE:
|
||||||
|
yield from hassio.send_command(
|
||||||
|
"/addons/{}/update".format(addon), payload=version,
|
||||||
|
timeout=LONG_TASK_TIMEOUT)
|
||||||
|
|
||||||
|
descriptions = yield from hass.loop.run_in_executor(
|
||||||
|
None, load_yaml_config_file, os.path.join(
|
||||||
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
|
for service, schema in SERVICE_MAP.items():
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, service, async_service_handler,
|
||||||
|
descriptions[DOMAIN][service], schema=schema)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class HassIO(object):
|
||||||
|
"""Small API wrapper for HassIO."""
|
||||||
|
|
||||||
|
def __init__(self, loop, websession, ip):
|
||||||
|
"""Initialze HassIO api."""
|
||||||
|
self.loop = loop
|
||||||
|
self.websession = websession
|
||||||
|
self._ip = ip
|
||||||
|
|
||||||
|
def is_connected(self):
|
||||||
|
"""Return True if it connected to HassIO supervisor.
|
||||||
|
|
||||||
|
Return a coroutine.
|
||||||
|
"""
|
||||||
|
return self.send_command("/supervisor/ping")
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def send_command(self, cmd, payload=None, timeout=DEFAULT_TIMEOUT):
|
||||||
|
"""Send request to API."""
|
||||||
|
answer = yield from self.send_raw(cmd, payload=payload)
|
||||||
|
if answer['result'] == 'ok':
|
||||||
|
return answer['data'] if answer['data'] else True
|
||||||
|
|
||||||
|
_LOGGER.error("%s return error %s.", cmd, answer['message'])
|
||||||
|
return False
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def send_raw(self, cmd, payload=None, timeout=DEFAULT_TIMEOUT):
|
||||||
|
"""Send raw request to API."""
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(timeout, loop=self.loop):
|
||||||
|
request = yield from self.websession.get(
|
||||||
|
"http://{}{}".format(self._ip, cmd),
|
||||||
|
timeout=None, json=payload
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.status != 200:
|
||||||
|
_LOGGER.error("%s return code %d.", cmd, request.status)
|
||||||
|
return
|
||||||
|
|
||||||
|
return (yield from request.json())
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.error("Timeout on api request %s.", cmd)
|
||||||
|
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
_LOGGER.error("Client error on api request %s.", cmd)
|
||||||
|
|
||||||
|
|
||||||
|
class HassIOBaseView(HomeAssistantView):
|
||||||
|
"""HassIO view to handle base part."""
|
||||||
|
|
||||||
|
requires_auth = True
|
||||||
|
|
||||||
|
def __init__(self, hassio, base):
|
||||||
|
"""Initialize a hassio base view."""
|
||||||
|
self.hassio = hassio
|
||||||
|
self._url_info = "/{}/info".format(base)
|
||||||
|
|
||||||
|
self.url = "/api/hassio/{}".format(base)
|
||||||
|
self.name = "api:hassio:{}".format(base)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def get(self, request):
|
||||||
|
"""Get base data."""
|
||||||
|
data = yield from self.hassio.send_command(self._url_info)
|
||||||
|
if not data:
|
||||||
|
raise HTTPBadGateway()
|
||||||
|
return web.json_response(data)
|
||||||
|
|
||||||
|
|
||||||
|
class HassIOBaseEditView(HassIOBaseView):
|
||||||
|
"""HassIO view to handle base with options support."""
|
||||||
|
|
||||||
|
def __init__(self, hassio, base):
|
||||||
|
"""Initialize a hassio base edit view."""
|
||||||
|
super().__init__(hassio, base)
|
||||||
|
self._url_options = "/{}/options".format(base)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def post(self, request):
|
||||||
|
"""Set options on host."""
|
||||||
|
data = yield from request.json()
|
||||||
|
|
||||||
|
response = yield from self.hassio.send_raw(
|
||||||
|
self._url_options, payload=data)
|
||||||
|
if not response:
|
||||||
|
raise HTTPBadGateway()
|
||||||
|
return web.json_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
class HassIOAddonsView(HomeAssistantView):
|
||||||
|
"""HassIO view to handle addons part."""
|
||||||
|
|
||||||
|
requires_auth = True
|
||||||
|
url = "/api/hassio/addons/{addon}"
|
||||||
|
name = "api:hassio:addons"
|
||||||
|
|
||||||
|
def __init__(self, hassio):
|
||||||
|
"""Initialize a hassio addon view."""
|
||||||
|
self.hassio = hassio
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def get(self, request, addon):
|
||||||
|
"""Get addon data."""
|
||||||
|
data = yield from self.hassio.send_command(
|
||||||
|
"/addons/{}/info".format(addon))
|
||||||
|
if not data:
|
||||||
|
raise HTTPBadGateway()
|
||||||
|
return web.json_response(data)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def post(self, request, addon):
|
||||||
|
"""Set options on host."""
|
||||||
|
data = yield from request.json()
|
||||||
|
|
||||||
|
response = yield from self.hassio.send_raw(
|
||||||
|
"/addons/{}/options".format(addon), payload=data)
|
||||||
|
if not response:
|
||||||
|
raise HTTPBadGateway()
|
||||||
|
return web.json_response(response)
|
|
@ -22,7 +22,7 @@ from homeassistant.helpers.event import track_time_interval
|
||||||
from homeassistant.config import load_yaml_config_file
|
from homeassistant.config import load_yaml_config_file
|
||||||
|
|
||||||
DOMAIN = 'homematic'
|
DOMAIN = 'homematic'
|
||||||
REQUIREMENTS = ["pyhomematic==0.1.22"]
|
REQUIREMENTS = ["pyhomematic==0.1.24"]
|
||||||
|
|
||||||
SCAN_INTERVAL_HUB = timedelta(seconds=300)
|
SCAN_INTERVAL_HUB = timedelta(seconds=300)
|
||||||
SCAN_INTERVAL_VARIABLES = timedelta(seconds=30)
|
SCAN_INTERVAL_VARIABLES = timedelta(seconds=30)
|
||||||
|
@ -82,7 +82,12 @@ HM_ATTRIBUTE_SUPPORT = {
|
||||||
'RSSI_DEVICE': ['rssi', {}],
|
'RSSI_DEVICE': ['rssi', {}],
|
||||||
'VALVE_STATE': ['valve', {}],
|
'VALVE_STATE': ['valve', {}],
|
||||||
'BATTERY_STATE': ['battery', {}],
|
'BATTERY_STATE': ['battery', {}],
|
||||||
'CONTROL_MODE': ['mode', {0: 'Auto', 1: 'Manual', 2: 'Away', 3: 'Boost'}],
|
'CONTROL_MODE': ['mode', {0: 'Auto',
|
||||||
|
1: 'Manual',
|
||||||
|
2: 'Away',
|
||||||
|
3: 'Boost',
|
||||||
|
4: 'Comfort',
|
||||||
|
5: 'Lowering'}],
|
||||||
'POWER': ['power', {}],
|
'POWER': ['power', {}],
|
||||||
'CURRENT': ['current', {}],
|
'CURRENT': ['current', {}],
|
||||||
'VOLTAGE': ['voltage', {}],
|
'VOLTAGE': ['voltage', {}],
|
||||||
|
|
|
@ -9,7 +9,6 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import ssl
|
import ssl
|
||||||
from ipaddress import ip_network
|
from ipaddress import ip_network
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -31,11 +30,13 @@ from .const import (
|
||||||
KEY_USE_X_FORWARDED_FOR, KEY_TRUSTED_NETWORKS,
|
KEY_USE_X_FORWARDED_FOR, KEY_TRUSTED_NETWORKS,
|
||||||
KEY_BANS_ENABLED, KEY_LOGIN_THRESHOLD,
|
KEY_BANS_ENABLED, KEY_LOGIN_THRESHOLD,
|
||||||
KEY_DEVELOPMENT, KEY_AUTHENTICATED)
|
KEY_DEVELOPMENT, KEY_AUTHENTICATED)
|
||||||
from .static import FILE_SENDER, CACHING_FILE_SENDER, staticresource_middleware
|
from .static import (
|
||||||
|
staticresource_middleware, CachingFileResponse, CachingStaticResource)
|
||||||
from .util import get_real_ip
|
from .util import get_real_ip
|
||||||
|
|
||||||
|
REQUIREMENTS = ['aiohttp_cors==0.5.2']
|
||||||
|
|
||||||
DOMAIN = 'http'
|
DOMAIN = 'http'
|
||||||
REQUIREMENTS = ('aiohttp_cors==0.5.0',)
|
|
||||||
|
|
||||||
CONF_API_PASSWORD = 'api_password'
|
CONF_API_PASSWORD = 'api_password'
|
||||||
CONF_SERVER_HOST = 'server_host'
|
CONF_SERVER_HOST = 'server_host'
|
||||||
|
@ -187,7 +188,7 @@ class HomeAssistantWSGI(object):
|
||||||
if is_ban_enabled:
|
if is_ban_enabled:
|
||||||
middlewares.insert(0, ban_middleware)
|
middlewares.insert(0, ban_middleware)
|
||||||
|
|
||||||
self.app = web.Application(middlewares=middlewares, loop=hass.loop)
|
self.app = web.Application(middlewares=middlewares)
|
||||||
self.app['hass'] = hass
|
self.app['hass'] = hass
|
||||||
self.app[KEY_USE_X_FORWARDED_FOR] = use_x_forwarded_for
|
self.app[KEY_USE_X_FORWARDED_FOR] = use_x_forwarded_for
|
||||||
self.app[KEY_TRUSTED_NETWORKS] = trusted_networks
|
self.app[KEY_TRUSTED_NETWORKS] = trusted_networks
|
||||||
|
@ -255,31 +256,39 @@ class HomeAssistantWSGI(object):
|
||||||
|
|
||||||
self.app.router.add_route('GET', url, redirect)
|
self.app.router.add_route('GET', url, redirect)
|
||||||
|
|
||||||
def register_static_path(self, url_root, path, cache_length=31):
|
def register_static_path(self, url_path, path, cache_headers=True):
|
||||||
"""Register a folder to serve as a static path.
|
"""Register a folder or file to serve as a static path."""
|
||||||
|
|
||||||
Specify optional cache length of asset in days.
|
|
||||||
"""
|
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
self.app.router.add_static(url_root, path)
|
if cache_headers:
|
||||||
|
resource = CachingStaticResource
|
||||||
|
else:
|
||||||
|
resource = web.StaticResource
|
||||||
|
|
||||||
|
self.app.router.register_resource(resource(url_path, path))
|
||||||
return
|
return
|
||||||
|
|
||||||
filepath = Path(path)
|
if cache_headers:
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def serve_file(request):
|
def serve_file(request):
|
||||||
"""Serve file from disk."""
|
"""Serve file from disk."""
|
||||||
res = yield from CACHING_FILE_SENDER.send(request, filepath)
|
return CachingFileResponse(path)
|
||||||
return res
|
else:
|
||||||
|
@asyncio.coroutine
|
||||||
|
def serve_file(request):
|
||||||
|
"""Serve file from disk."""
|
||||||
|
return web.FileResponse(path)
|
||||||
|
|
||||||
# aiohttp supports regex matching for variables. Using that as temp
|
# aiohttp supports regex matching for variables. Using that as temp
|
||||||
# to work around cache busting MD5.
|
# to work around cache busting MD5.
|
||||||
# Turns something like /static/dev-panel.html into
|
# Turns something like /static/dev-panel.html into
|
||||||
# /static/{filename:dev-panel(-[a-z0-9]{32}|)\.html}
|
# /static/{filename:dev-panel(-[a-z0-9]{32}|)\.html}
|
||||||
base, ext = url_root.rsplit('.', 1)
|
base, ext = os.path.splitext(url_path)
|
||||||
|
if ext:
|
||||||
base, file = base.rsplit('/', 1)
|
base, file = base.rsplit('/', 1)
|
||||||
regex = r"{}(-[a-z0-9]{{32}}|)\.{}".format(file, ext)
|
regex = r"{}(-[a-z0-9]{{32}}|){}".format(file, ext)
|
||||||
url_pattern = "{}/{{filename:{}}}".format(base, regex)
|
url_pattern = "{}/{{filename:{}}}".format(base, regex)
|
||||||
|
else:
|
||||||
|
url_pattern = url_path
|
||||||
|
|
||||||
self.app.router.add_route('GET', url_pattern, serve_file)
|
self.app.router.add_route('GET', url_pattern, serve_file)
|
||||||
|
|
||||||
|
@ -318,7 +327,7 @@ class HomeAssistantWSGI(object):
|
||||||
# re-register all redirects, views, static paths.
|
# re-register all redirects, views, static paths.
|
||||||
self.app._frozen = True # pylint: disable=protected-access
|
self.app._frozen = True # pylint: disable=protected-access
|
||||||
|
|
||||||
self._handler = self.app.make_handler()
|
self._handler = self.app.make_handler(loop=self.hass.loop)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.server = yield from self.hass.loop.create_server(
|
self.server = yield from self.hass.loop.create_server(
|
||||||
|
@ -365,8 +374,7 @@ class HomeAssistantView(object):
|
||||||
def file(self, request, fil):
|
def file(self, request, fil):
|
||||||
"""Return a file."""
|
"""Return a file."""
|
||||||
assert isinstance(fil, str), 'only string paths allowed'
|
assert isinstance(fil, str), 'only string paths allowed'
|
||||||
response = yield from FILE_SENDER.send(request, Path(fil))
|
return web.FileResponse(fil)
|
||||||
return response
|
|
||||||
|
|
||||||
def register(self, router):
|
def register(self, router):
|
||||||
"""Register the view with a router."""
|
"""Register the view with a router."""
|
||||||
|
|
|
@ -3,14 +3,45 @@ import asyncio
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from aiohttp import hdrs
|
from aiohttp import hdrs
|
||||||
from aiohttp.file_sender import FileSender
|
from aiohttp.web import FileResponse
|
||||||
|
from aiohttp.web_exceptions import HTTPNotFound
|
||||||
from aiohttp.web_urldispatcher import StaticResource
|
from aiohttp.web_urldispatcher import StaticResource
|
||||||
|
from yarl import unquote
|
||||||
|
|
||||||
from .const import KEY_DEVELOPMENT
|
from .const import KEY_DEVELOPMENT
|
||||||
|
|
||||||
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
class CachingFileSender(FileSender):
|
class CachingStaticResource(StaticResource):
|
||||||
|
"""Static Resource handler that will add cache headers."""
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _handle(self, request):
|
||||||
|
filename = unquote(request.match_info['filename'])
|
||||||
|
try:
|
||||||
|
# PyLint is wrong about resolve not being a member.
|
||||||
|
# pylint: disable=no-member
|
||||||
|
filepath = self._directory.joinpath(filename).resolve()
|
||||||
|
if not self._follow_symlinks:
|
||||||
|
filepath.relative_to(self._directory)
|
||||||
|
except (ValueError, FileNotFoundError) as error:
|
||||||
|
# relatively safe
|
||||||
|
raise HTTPNotFound() from error
|
||||||
|
except Exception as error:
|
||||||
|
# perm error or other kind!
|
||||||
|
request.app.logger.exception(error)
|
||||||
|
raise HTTPNotFound() from error
|
||||||
|
|
||||||
|
if filepath.is_dir():
|
||||||
|
return (yield from super()._handle(request))
|
||||||
|
elif filepath.is_file():
|
||||||
|
return CachingFileResponse(filepath, chunk_size=self._chunk_size)
|
||||||
|
else:
|
||||||
|
raise HTTPNotFound
|
||||||
|
|
||||||
|
|
||||||
|
class CachingFileResponse(FileResponse):
|
||||||
"""FileSender class that caches output if not in dev mode."""
|
"""FileSender class that caches output if not in dev mode."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -20,46 +51,34 @@ class CachingFileSender(FileSender):
|
||||||
orig_sendfile = self._sendfile
|
orig_sendfile = self._sendfile
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def sendfile(request, resp, fobj, count):
|
def sendfile(request, fobj, count):
|
||||||
"""Sendfile that includes a cache header."""
|
"""Sendfile that includes a cache header."""
|
||||||
if not request.app[KEY_DEVELOPMENT]:
|
if not request.app[KEY_DEVELOPMENT]:
|
||||||
cache_time = 31 * 86400 # = 1 month
|
cache_time = 31 * 86400 # = 1 month
|
||||||
resp.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format(
|
self.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format(
|
||||||
cache_time)
|
cache_time)
|
||||||
|
|
||||||
yield from orig_sendfile(request, resp, fobj, count)
|
yield from orig_sendfile(request, fobj, count)
|
||||||
|
|
||||||
# Overwriting like this because __init__ can change implementation.
|
# Overwriting like this because __init__ can change implementation.
|
||||||
self._sendfile = sendfile
|
self._sendfile = sendfile
|
||||||
|
|
||||||
|
|
||||||
FILE_SENDER = FileSender()
|
|
||||||
CACHING_FILE_SENDER = CachingFileSender()
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def staticresource_middleware(app, handler):
|
def staticresource_middleware(app, handler):
|
||||||
"""Enhance StaticResourceHandler middleware.
|
"""Middleware to strip out fingerprint from fingerprinted assets."""
|
||||||
|
|
||||||
Adds gzip encoding and fingerprinting matching.
|
|
||||||
"""
|
|
||||||
inst = getattr(handler, '__self__', None)
|
|
||||||
if not isinstance(inst, StaticResource):
|
|
||||||
return handler
|
|
||||||
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
inst._file_sender = CACHING_FILE_SENDER
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def static_middleware_handler(request):
|
def static_middleware_handler(request):
|
||||||
"""Strip out fingerprints from resource names."""
|
"""Strip out fingerprints from resource names."""
|
||||||
|
if not request.path.startswith('/static/'):
|
||||||
|
return handler(request)
|
||||||
|
|
||||||
fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
|
fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
|
||||||
|
|
||||||
if fingerprinted:
|
if fingerprinted:
|
||||||
request.match_info['filename'] = \
|
request.match_info['filename'] = \
|
||||||
'{}.{}'.format(*fingerprinted.groups())
|
'{}.{}'.format(*fingerprinted.groups())
|
||||||
|
|
||||||
resp = yield from handler(request)
|
return handler(request)
|
||||||
return resp
|
|
||||||
|
|
||||||
return static_middleware_handler
|
return static_middleware_handler
|
||||||
|
|
|
@ -112,8 +112,6 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity):
|
||||||
|
|
||||||
params['image_bytes'] = str(b64encode(image), 'utf-8')
|
params['image_bytes'] = str(b64encode(image), 'utf-8')
|
||||||
|
|
||||||
data = None
|
|
||||||
request = None
|
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(self.timeout, loop=self.hass.loop):
|
with async_timeout.timeout(self.timeout, loop=self.hass.loop):
|
||||||
request = yield from websession.post(
|
request = yield from websession.post(
|
||||||
|
@ -127,14 +125,10 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity):
|
||||||
request.status, data.get('error'))
|
request.status, data.get('error'))
|
||||||
return
|
return
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.error("Timeout for openalpr api.")
|
_LOGGER.error("Timeout for openalpr api.")
|
||||||
return
|
return
|
||||||
|
|
||||||
finally:
|
|
||||||
if request is not None:
|
|
||||||
yield from request.release()
|
|
||||||
|
|
||||||
# processing api data
|
# processing api data
|
||||||
vehicles = 0
|
vehicles = 0
|
||||||
result = {}
|
result = {}
|
||||||
|
|
|
@ -30,13 +30,11 @@ SERVICE_SCHEMA = vol.Schema({
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
})
|
})
|
||||||
|
|
||||||
DEFAULT_CONFIG = {CONF_INITIAL: DEFAULT_INITIAL}
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
cv.slug: vol.Any({
|
cv.slug: vol.Any({
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.boolean,
|
vol.Optional(CONF_INITIAL): cv.boolean,
|
||||||
vol.Optional(CONF_ICON): cv.icon,
|
vol.Optional(CONF_ICON): cv.icon,
|
||||||
}, None)
|
}, None)
|
||||||
})
|
})
|
||||||
|
@ -72,13 +70,13 @@ def async_setup(hass, config):
|
||||||
|
|
||||||
for object_id, cfg in config[DOMAIN].items():
|
for object_id, cfg in config[DOMAIN].items():
|
||||||
if not cfg:
|
if not cfg:
|
||||||
cfg = DEFAULT_CONFIG
|
cfg = {}
|
||||||
|
|
||||||
name = cfg.get(CONF_NAME)
|
name = cfg.get(CONF_NAME)
|
||||||
state = cfg.get(CONF_INITIAL)
|
initial = cfg.get(CONF_INITIAL)
|
||||||
icon = cfg.get(CONF_ICON)
|
icon = cfg.get(CONF_ICON)
|
||||||
|
|
||||||
entities.append(InputBoolean(object_id, name, state, icon))
|
entities.append(InputBoolean(object_id, name, initial, icon))
|
||||||
|
|
||||||
if not entities:
|
if not entities:
|
||||||
return False
|
return False
|
||||||
|
@ -113,11 +111,11 @@ def async_setup(hass, config):
|
||||||
class InputBoolean(ToggleEntity):
|
class InputBoolean(ToggleEntity):
|
||||||
"""Representation of a boolean input."""
|
"""Representation of a boolean input."""
|
||||||
|
|
||||||
def __init__(self, object_id, name, state, icon):
|
def __init__(self, object_id, name, initial, icon):
|
||||||
"""Initialize a boolean input."""
|
"""Initialize a boolean input."""
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state = state
|
self._state = initial
|
||||||
self._icon = icon
|
self._icon = icon
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -143,10 +141,12 @@ class InputBoolean(ToggleEntity):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Called when entity about to be added to hass."""
|
"""Called when entity about to be added to hass."""
|
||||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
# If not None, we got an initial value.
|
||||||
if not state:
|
if self._state is not None:
|
||||||
return
|
return
|
||||||
self._state = state.state == STATE_ON
|
|
||||||
|
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||||
|
self._state = state and state.state == STATE_ON
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_turn_on(self, **kwargs):
|
def async_turn_on(self, **kwargs):
|
||||||
|
|
|
@ -58,10 +58,10 @@ SERVICE_SET_OPTIONS_SCHEMA = vol.Schema({
|
||||||
def _cv_input_select(cfg):
|
def _cv_input_select(cfg):
|
||||||
"""Config validation helper for input select (Voluptuous)."""
|
"""Config validation helper for input select (Voluptuous)."""
|
||||||
options = cfg[CONF_OPTIONS]
|
options = cfg[CONF_OPTIONS]
|
||||||
state = cfg.get(CONF_INITIAL, options[0])
|
initial = cfg.get(CONF_INITIAL)
|
||||||
if state not in options:
|
if initial is not None and initial not in options:
|
||||||
raise vol.Invalid('initial state "{}" is not part of the options: {}'
|
raise vol.Invalid('initial state "{}" is not part of the options: {}'
|
||||||
.format(state, ','.join(options)))
|
.format(initial, ','.join(options)))
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
@ -117,9 +117,9 @@ def async_setup(hass, config):
|
||||||
for object_id, cfg in config[DOMAIN].items():
|
for object_id, cfg in config[DOMAIN].items():
|
||||||
name = cfg.get(CONF_NAME)
|
name = cfg.get(CONF_NAME)
|
||||||
options = cfg.get(CONF_OPTIONS)
|
options = cfg.get(CONF_OPTIONS)
|
||||||
state = cfg.get(CONF_INITIAL, options[0])
|
initial = cfg.get(CONF_INITIAL)
|
||||||
icon = cfg.get(CONF_ICON)
|
icon = cfg.get(CONF_ICON)
|
||||||
entities.append(InputSelect(object_id, name, state, options, icon))
|
entities.append(InputSelect(object_id, name, initial, options, icon))
|
||||||
|
|
||||||
if not entities:
|
if not entities:
|
||||||
return False
|
return False
|
||||||
|
@ -187,22 +187,24 @@ def async_setup(hass, config):
|
||||||
class InputSelect(Entity):
|
class InputSelect(Entity):
|
||||||
"""Representation of a select input."""
|
"""Representation of a select input."""
|
||||||
|
|
||||||
def __init__(self, object_id, name, state, options, icon):
|
def __init__(self, object_id, name, initial, options, icon):
|
||||||
"""Initialize a select input."""
|
"""Initialize a select input."""
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||||
self._name = name
|
self._name = name
|
||||||
self._current_option = state
|
self._current_option = initial
|
||||||
self._options = options
|
self._options = options
|
||||||
self._icon = icon
|
self._icon = icon
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Called when entity about to be added to hass."""
|
"""Called when entity about to be added to hass."""
|
||||||
|
if self._current_option is not None:
|
||||||
|
return
|
||||||
|
|
||||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||||
if not state:
|
if not state or state.state not in self._options:
|
||||||
return
|
self._current_option = self._options[0]
|
||||||
if state.state not in self._options:
|
else:
|
||||||
return
|
|
||||||
self._current_option = state.state
|
self._current_option = state.state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -45,11 +45,10 @@ def _cv_input_slider(cfg):
|
||||||
if minimum >= maximum:
|
if minimum >= maximum:
|
||||||
raise vol.Invalid('Maximum ({}) is not greater than minimum ({})'
|
raise vol.Invalid('Maximum ({}) is not greater than minimum ({})'
|
||||||
.format(minimum, maximum))
|
.format(minimum, maximum))
|
||||||
state = cfg.get(CONF_INITIAL, minimum)
|
state = cfg.get(CONF_INITIAL)
|
||||||
if state < minimum or state > maximum:
|
if state is not None and (state < minimum or state > maximum):
|
||||||
raise vol.Invalid('Initial value {} not in range {}-{}'
|
raise vol.Invalid('Initial value {} not in range {}-{}'
|
||||||
.format(state, minimum, maximum))
|
.format(state, minimum, maximum))
|
||||||
cfg[CONF_INITIAL] = state
|
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
@ -88,12 +87,12 @@ def async_setup(hass, config):
|
||||||
name = cfg.get(CONF_NAME)
|
name = cfg.get(CONF_NAME)
|
||||||
minimum = cfg.get(CONF_MIN)
|
minimum = cfg.get(CONF_MIN)
|
||||||
maximum = cfg.get(CONF_MAX)
|
maximum = cfg.get(CONF_MAX)
|
||||||
state = cfg.get(CONF_INITIAL, minimum)
|
initial = cfg.get(CONF_INITIAL)
|
||||||
step = cfg.get(CONF_STEP)
|
step = cfg.get(CONF_STEP)
|
||||||
icon = cfg.get(CONF_ICON)
|
icon = cfg.get(CONF_ICON)
|
||||||
unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT)
|
unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
|
||||||
entities.append(InputSlider(object_id, name, state, minimum, maximum,
|
entities.append(InputSlider(object_id, name, initial, minimum, maximum,
|
||||||
step, icon, unit))
|
step, icon, unit))
|
||||||
|
|
||||||
if not entities:
|
if not entities:
|
||||||
|
@ -120,12 +119,12 @@ def async_setup(hass, config):
|
||||||
class InputSlider(Entity):
|
class InputSlider(Entity):
|
||||||
"""Represent an slider."""
|
"""Represent an slider."""
|
||||||
|
|
||||||
def __init__(self, object_id, name, state, minimum, maximum, step, icon,
|
def __init__(self, object_id, name, initial, minimum, maximum, step, icon,
|
||||||
unit):
|
unit):
|
||||||
"""Initialize a select input."""
|
"""Initialize a select input."""
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||||
self._name = name
|
self._name = name
|
||||||
self._current_value = state
|
self._current_value = initial
|
||||||
self._minimum = minimum
|
self._minimum = minimum
|
||||||
self._maximum = maximum
|
self._maximum = maximum
|
||||||
self._step = step
|
self._step = step
|
||||||
|
@ -169,14 +168,17 @@ class InputSlider(Entity):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Called when entity about to be added to hass."""
|
"""Called when entity about to be added to hass."""
|
||||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
if self._current_value is not None:
|
||||||
if not state:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
num_value = float(state.state)
|
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||||
if num_value < self._minimum or num_value > self._maximum:
|
value = state and float(state.state)
|
||||||
return
|
|
||||||
self._current_value = num_value
|
# Check against False because value can be 0
|
||||||
|
if value is not False and self._minimum < value < self._maximum:
|
||||||
|
self._current_value = value
|
||||||
|
else:
|
||||||
|
self._current_value = self._minimum
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_select_value(self, value):
|
def async_select_value(self, value):
|
||||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.components.light import (
|
||||||
PLATFORM_SCHEMA)
|
PLATFORM_SCHEMA)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['flux_led==0.15']
|
REQUIREMENTS = ['flux_led==0.18']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
|
||||||
|
|
||||||
PHUE_CONFIG_FILE = 'phue.conf'
|
PHUE_CONFIG_FILE = 'phue.conf'
|
||||||
|
|
||||||
SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION | SUPPORT_FLASH)
|
SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION)
|
||||||
SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS)
|
SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS)
|
||||||
SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP)
|
SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP)
|
||||||
SUPPORT_HUE_COLOR = (SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT |
|
SUPPORT_HUE_COLOR = (SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT |
|
||||||
|
@ -58,6 +58,7 @@ SUPPORT_HUE = {
|
||||||
'Extended color light': SUPPORT_HUE_EXTENDED,
|
'Extended color light': SUPPORT_HUE_EXTENDED,
|
||||||
'Color light': SUPPORT_HUE_COLOR,
|
'Color light': SUPPORT_HUE_COLOR,
|
||||||
'Dimmable light': SUPPORT_HUE_DIMMABLE,
|
'Dimmable light': SUPPORT_HUE_DIMMABLE,
|
||||||
|
'On/Off plug-in unit': SUPPORT_HUE_ON_OFF,
|
||||||
'Color temperature light': SUPPORT_HUE_COLOR_TEMP
|
'Color temperature light': SUPPORT_HUE_COLOR_TEMP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -175,7 +175,7 @@ class InsteonLocalDimmerDevice(Light):
|
||||||
if ATTR_BRIGHTNESS in kwargs:
|
if ATTR_BRIGHTNESS in kwargs:
|
||||||
brightness = int(kwargs[ATTR_BRIGHTNESS]) / 255 * 100
|
brightness = int(kwargs[ATTR_BRIGHTNESS]) / 255 * 100
|
||||||
|
|
||||||
self.node.on(brightness)
|
self.node.change_level(brightness)
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn device off."""
|
"""Turn device off."""
|
||||||
|
|
|
@ -36,7 +36,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
InsteonPLMDimmerDevice(hass, plm, address, name, dimmable)
|
InsteonPLMDimmerDevice(hass, plm, address, name, dimmable)
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.async_add_job(async_add_devices(device_list))
|
async_add_devices(device_list)
|
||||||
|
|
||||||
|
|
||||||
class InsteonPLMDimmerDevice(Light):
|
class InsteonPLMDimmerDevice(Light):
|
||||||
|
|
|
@ -7,6 +7,7 @@ https://home-assistant.io/components/light.lifx/
|
||||||
import colorsys
|
import colorsys
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import sys
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
@ -51,6 +52,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Setup the LIFX platform."""
|
"""Setup the LIFX platform."""
|
||||||
import aiolifx
|
import aiolifx
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
_LOGGER.warning('The lifx platform is known to not work on Windows. '
|
||||||
|
'Consider using the lifx_legacy platform instead.')
|
||||||
|
|
||||||
server_addr = config.get(CONF_SERVER)
|
server_addr = config.get(CONF_SERVER)
|
||||||
|
|
||||||
lifx_manager = LIFXManager(hass, async_add_devices)
|
lifx_manager = LIFXManager(hass, async_add_devices)
|
||||||
|
@ -90,7 +95,7 @@ class LIFXManager(object):
|
||||||
entity = LIFXLight(device)
|
entity = LIFXLight(device)
|
||||||
_LOGGER.debug("%s register READY", entity.ipaddr)
|
_LOGGER.debug("%s register READY", entity.ipaddr)
|
||||||
self.entities[device.mac_addr] = entity
|
self.entities[device.mac_addr] = entity
|
||||||
self.hass.async_add_job(self.async_add_devices, [entity])
|
self.async_add_devices([entity])
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def unregister(self, device):
|
def unregister(self, device):
|
||||||
|
|
|
@ -0,0 +1,280 @@
|
||||||
|
"""
|
||||||
|
Support for the LIFX platform that implements lights.
|
||||||
|
|
||||||
|
This is a legacy platform, included because the current lifx platform does
|
||||||
|
not yet support Windows.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/light.lifx/
|
||||||
|
"""
|
||||||
|
import colorsys
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.light import (
|
||||||
|
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION,
|
||||||
|
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
|
||||||
|
SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
|
||||||
|
from homeassistant.helpers.event import track_time_change
|
||||||
|
from homeassistant.util.color import (
|
||||||
|
color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
REQUIREMENTS = ['liffylights==0.9.4']
|
||||||
|
|
||||||
|
BYTE_MAX = 255
|
||||||
|
|
||||||
|
CONF_BROADCAST = 'broadcast'
|
||||||
|
CONF_SERVER = 'server'
|
||||||
|
|
||||||
|
SHORT_MAX = 65535
|
||||||
|
|
||||||
|
TEMP_MAX = 9000
|
||||||
|
TEMP_MAX_HASS = 500
|
||||||
|
TEMP_MIN = 2500
|
||||||
|
TEMP_MIN_HASS = 154
|
||||||
|
|
||||||
|
SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR |
|
||||||
|
SUPPORT_TRANSITION)
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_SERVER, default=None): cv.string,
|
||||||
|
vol.Optional(CONF_BROADCAST, default=None): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the LIFX platform."""
|
||||||
|
server_addr = config.get(CONF_SERVER)
|
||||||
|
broadcast_addr = config.get(CONF_BROADCAST)
|
||||||
|
|
||||||
|
lifx_library = LIFX(add_devices, server_addr, broadcast_addr)
|
||||||
|
|
||||||
|
# Register our poll service
|
||||||
|
track_time_change(hass, lifx_library.poll, second=[10, 40])
|
||||||
|
|
||||||
|
lifx_library.probe()
|
||||||
|
|
||||||
|
|
||||||
|
class LIFX(object):
|
||||||
|
"""Representation of a LIFX light."""
|
||||||
|
|
||||||
|
def __init__(self, add_devices_callback, server_addr=None,
|
||||||
|
broadcast_addr=None):
|
||||||
|
"""Initialize the light."""
|
||||||
|
import liffylights
|
||||||
|
|
||||||
|
self._devices = []
|
||||||
|
|
||||||
|
self._add_devices_callback = add_devices_callback
|
||||||
|
|
||||||
|
self._liffylights = liffylights.LiffyLights(
|
||||||
|
self.on_device, self.on_power, self.on_color, server_addr,
|
||||||
|
broadcast_addr)
|
||||||
|
|
||||||
|
def find_bulb(self, ipaddr):
|
||||||
|
"""Search for bulbs."""
|
||||||
|
bulb = None
|
||||||
|
for device in self._devices:
|
||||||
|
if device.ipaddr == ipaddr:
|
||||||
|
bulb = device
|
||||||
|
break
|
||||||
|
return bulb
|
||||||
|
|
||||||
|
def on_device(self, ipaddr, name, power, hue, sat, bri, kel):
|
||||||
|
"""Initialize the light."""
|
||||||
|
bulb = self.find_bulb(ipaddr)
|
||||||
|
|
||||||
|
if bulb is None:
|
||||||
|
_LOGGER.debug("new bulb %s %s %d %d %d %d %d",
|
||||||
|
ipaddr, name, power, hue, sat, bri, kel)
|
||||||
|
bulb = LIFXLight(
|
||||||
|
self._liffylights, ipaddr, name, power, hue, sat, bri, kel)
|
||||||
|
self._devices.append(bulb)
|
||||||
|
self._add_devices_callback([bulb])
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("update bulb %s %s %d %d %d %d %d",
|
||||||
|
ipaddr, name, power, hue, sat, bri, kel)
|
||||||
|
bulb.set_power(power)
|
||||||
|
bulb.set_color(hue, sat, bri, kel)
|
||||||
|
bulb.schedule_update_ha_state()
|
||||||
|
|
||||||
|
def on_color(self, ipaddr, hue, sat, bri, kel):
|
||||||
|
"""Initialize the light."""
|
||||||
|
bulb = self.find_bulb(ipaddr)
|
||||||
|
|
||||||
|
if bulb is not None:
|
||||||
|
bulb.set_color(hue, sat, bri, kel)
|
||||||
|
bulb.schedule_update_ha_state()
|
||||||
|
|
||||||
|
def on_power(self, ipaddr, power):
|
||||||
|
"""Initialize the light."""
|
||||||
|
bulb = self.find_bulb(ipaddr)
|
||||||
|
|
||||||
|
if bulb is not None:
|
||||||
|
bulb.set_power(power)
|
||||||
|
bulb.schedule_update_ha_state()
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def poll(self, now):
|
||||||
|
"""Polling for the light."""
|
||||||
|
self.probe()
|
||||||
|
|
||||||
|
def probe(self, address=None):
|
||||||
|
"""Probe the light."""
|
||||||
|
self._liffylights.probe(address)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_rgb_to_hsv(rgb):
|
||||||
|
"""Convert Home Assistant RGB values to HSV values."""
|
||||||
|
red, green, blue = [_ / BYTE_MAX for _ in rgb]
|
||||||
|
|
||||||
|
hue, saturation, brightness = colorsys.rgb_to_hsv(red, green, blue)
|
||||||
|
|
||||||
|
return [int(hue * SHORT_MAX),
|
||||||
|
int(saturation * SHORT_MAX),
|
||||||
|
int(brightness * SHORT_MAX)]
|
||||||
|
|
||||||
|
|
||||||
|
class LIFXLight(Light):
|
||||||
|
"""Representation of a LIFX light."""
|
||||||
|
|
||||||
|
def __init__(self, liffy, ipaddr, name, power, hue, saturation, brightness,
|
||||||
|
kelvin):
|
||||||
|
"""Initialize the light."""
|
||||||
|
_LOGGER.debug("LIFXLight: %s %s", ipaddr, name)
|
||||||
|
|
||||||
|
self._liffylights = liffy
|
||||||
|
self._ip = ipaddr
|
||||||
|
self.set_name(name)
|
||||||
|
self.set_power(power)
|
||||||
|
self.set_color(hue, saturation, brightness, kelvin)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No polling needed for LIFX light."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ipaddr(self):
|
||||||
|
"""Return the IP address of the device."""
|
||||||
|
return self._ip
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rgb_color(self):
|
||||||
|
"""Return the RGB value."""
|
||||||
|
_LOGGER.debug(
|
||||||
|
"rgb_color: [%d %d %d]", self._rgb[0], self._rgb[1], self._rgb[2])
|
||||||
|
return self._rgb
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
"""Return the brightness of this light between 0..255."""
|
||||||
|
brightness = int(self._bri / (BYTE_MAX + 1))
|
||||||
|
_LOGGER.debug("brightness: %d", brightness)
|
||||||
|
return brightness
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_temp(self):
|
||||||
|
"""Return the color temperature."""
|
||||||
|
temperature = color_temperature_kelvin_to_mired(self._kel)
|
||||||
|
|
||||||
|
_LOGGER.debug("color_temp: %d", temperature)
|
||||||
|
return temperature
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if device is on."""
|
||||||
|
_LOGGER.debug("is_on: %d", self._power)
|
||||||
|
return self._power != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag supported features."""
|
||||||
|
return SUPPORT_LIFX
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
"""Turn the device on."""
|
||||||
|
if ATTR_TRANSITION in kwargs:
|
||||||
|
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
||||||
|
else:
|
||||||
|
fade = 0
|
||||||
|
|
||||||
|
if ATTR_RGB_COLOR in kwargs:
|
||||||
|
hue, saturation, brightness = \
|
||||||
|
convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR])
|
||||||
|
else:
|
||||||
|
hue = self._hue
|
||||||
|
saturation = self._sat
|
||||||
|
brightness = self._bri
|
||||||
|
|
||||||
|
if ATTR_BRIGHTNESS in kwargs:
|
||||||
|
brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1)
|
||||||
|
else:
|
||||||
|
brightness = self._bri
|
||||||
|
|
||||||
|
if ATTR_COLOR_TEMP in kwargs:
|
||||||
|
kelvin = int(color_temperature_mired_to_kelvin(
|
||||||
|
kwargs[ATTR_COLOR_TEMP]))
|
||||||
|
else:
|
||||||
|
kelvin = self._kel
|
||||||
|
|
||||||
|
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
|
||||||
|
self._ip, self._power,
|
||||||
|
hue, saturation, brightness, kelvin, fade)
|
||||||
|
|
||||||
|
if self._power == 0:
|
||||||
|
self._liffylights.set_color(self._ip, hue, saturation,
|
||||||
|
brightness, kelvin, 0)
|
||||||
|
self._liffylights.set_power(self._ip, 65535, fade)
|
||||||
|
else:
|
||||||
|
self._liffylights.set_color(self._ip, hue, saturation,
|
||||||
|
brightness, kelvin, fade)
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
"""Turn the device off."""
|
||||||
|
if ATTR_TRANSITION in kwargs:
|
||||||
|
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
||||||
|
else:
|
||||||
|
fade = 0
|
||||||
|
|
||||||
|
_LOGGER.debug("turn_off: %s %d", self._ip, fade)
|
||||||
|
self._liffylights.set_power(self._ip, 0, fade)
|
||||||
|
|
||||||
|
def set_name(self, name):
|
||||||
|
"""Set name of the light."""
|
||||||
|
self._name = name
|
||||||
|
|
||||||
|
def set_power(self, power):
|
||||||
|
"""Set power state value."""
|
||||||
|
_LOGGER.debug("set_power: %d", power)
|
||||||
|
self._power = (power != 0)
|
||||||
|
|
||||||
|
def set_color(self, hue, sat, bri, kel):
|
||||||
|
"""Set color state values."""
|
||||||
|
self._hue = hue
|
||||||
|
self._sat = sat
|
||||||
|
self._bri = bri
|
||||||
|
self._kel = kel
|
||||||
|
|
||||||
|
red, green, blue = colorsys.hsv_to_rgb(hue / SHORT_MAX,
|
||||||
|
sat / SHORT_MAX,
|
||||||
|
bri / SHORT_MAX)
|
||||||
|
|
||||||
|
red = int(red * BYTE_MAX)
|
||||||
|
green = int(green * BYTE_MAX)
|
||||||
|
blue = int(blue * BYTE_MAX)
|
||||||
|
|
||||||
|
_LOGGER.debug("set_color: %d %d %d %d [%d %d %d]",
|
||||||
|
hue, sat, bri, kel, red, green, blue)
|
||||||
|
|
||||||
|
self._rgb = [red, green, blue]
|
|
@ -25,14 +25,13 @@ CONF_BRIDGES = 'bridges'
|
||||||
CONF_GROUPS = 'groups'
|
CONF_GROUPS = 'groups'
|
||||||
CONF_NUMBER = 'number'
|
CONF_NUMBER = 'number'
|
||||||
CONF_VERSION = 'version'
|
CONF_VERSION = 'version'
|
||||||
CONF_BRIDGE_LED = 'bridge_led'
|
|
||||||
|
|
||||||
DEFAULT_LED_TYPE = 'rgbw'
|
DEFAULT_LED_TYPE = 'rgbw'
|
||||||
DEFAULT_PORT = 5987
|
DEFAULT_PORT = 5987
|
||||||
DEFAULT_TRANSITION = 0
|
DEFAULT_TRANSITION = 0
|
||||||
DEFAULT_VERSION = 6
|
DEFAULT_VERSION = 6
|
||||||
|
|
||||||
LED_TYPE = ['rgbw', 'rgbww', 'white']
|
LED_TYPE = ['rgbw', 'rgbww', 'white', 'bridge-led']
|
||||||
|
|
||||||
RGB_BOUNDARY = 40
|
RGB_BOUNDARY = 40
|
||||||
|
|
||||||
|
@ -54,7 +53,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_VERSION,
|
vol.Optional(CONF_VERSION,
|
||||||
default=DEFAULT_VERSION): cv.positive_int,
|
default=DEFAULT_VERSION): cv.positive_int,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
vol.Optional(CONF_BRIDGE_LED, default=False): cv.boolean,
|
|
||||||
vol.Required(CONF_GROUPS): vol.All(cv.ensure_list, [
|
vol.Required(CONF_GROUPS): vol.All(cv.ensure_list, [
|
||||||
{
|
{
|
||||||
vol.Required(CONF_NAME): cv.string,
|
vol.Required(CONF_NAME): cv.string,
|
||||||
|
@ -116,8 +114,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
group_conf.get(CONF_NAME),
|
group_conf.get(CONF_NAME),
|
||||||
group_conf.get(CONF_TYPE, DEFAULT_LED_TYPE))
|
group_conf.get(CONF_TYPE, DEFAULT_LED_TYPE))
|
||||||
lights.append(LimitlessLEDGroup.factory(group))
|
lights.append(LimitlessLEDGroup.factory(group))
|
||||||
if bridge_conf.get(CONF_BRIDGE_LED) and bridge.bridge_led is not None:
|
|
||||||
lights.append(LimitlessLEDGroup.factory(bridge.bridge_led))
|
|
||||||
add_devices(lights)
|
add_devices(lights)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ class LiteJetLight(Light):
|
||||||
def _on_load_changed(self):
|
def _on_load_changed(self):
|
||||||
"""Called on a LiteJet thread when a load's state changes."""
|
"""Called on a LiteJet thread when a load's state changes."""
|
||||||
_LOGGER.debug("Updating due to notification for %s", self._name)
|
_LOGGER.debug("Updating due to notification for %s", self._name)
|
||||||
self._hass.async_add_job(self.async_update_ha_state(True))
|
self.schedule_update_ha_state(True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
|
|
|
@ -19,7 +19,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup Lutron Caseta lights."""
|
"""Setup Lutron Caseta lights."""
|
||||||
devs = []
|
devs = []
|
||||||
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
|
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
|
||||||
light_devices = bridge.get_devices_by_type("WallDimmer")
|
light_devices = bridge.get_devices_by_types(["WallDimmer", "PlugInDimmer"])
|
||||||
for light_device in light_devices:
|
for light_device in light_devices:
|
||||||
dev = LutronCasetaLight(light_device, bridge)
|
dev = LutronCasetaLight(light_device, bridge)
|
||||||
devs.append(dev)
|
devs.append(dev)
|
||||||
|
|
|
@ -148,16 +148,12 @@ class MqttJson(Light):
|
||||||
self._flash_times = flash_times
|
self._flash_times = flash_times
|
||||||
|
|
||||||
self._supported_features = (SUPPORT_TRANSITION | SUPPORT_FLASH)
|
self._supported_features = (SUPPORT_TRANSITION | SUPPORT_FLASH)
|
||||||
self._supported_features |= (rgb is not None and SUPPORT_RGB_COLOR)
|
self._supported_features |= (rgb and SUPPORT_RGB_COLOR)
|
||||||
self._supported_features |= (brightness is not None and
|
self._supported_features |= (brightness and SUPPORT_BRIGHTNESS)
|
||||||
SUPPORT_BRIGHTNESS)
|
self._supported_features |= (color_temp and SUPPORT_COLOR_TEMP)
|
||||||
self._supported_features |= (color_temp is not None and
|
self._supported_features |= (effect and SUPPORT_EFFECT)
|
||||||
SUPPORT_COLOR_TEMP)
|
self._supported_features |= (white_value and SUPPORT_WHITE_VALUE)
|
||||||
self._supported_features |= (effect is not None and
|
self._supported_features |= (xy and SUPPORT_XY_COLOR)
|
||||||
SUPPORT_EFFECT)
|
|
||||||
self._supported_features |= (white_value is not None and
|
|
||||||
SUPPORT_WHITE_VALUE)
|
|
||||||
self._supported_features |= (xy is not None and SUPPORT_XY_COLOR)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
|
|
|
@ -9,15 +9,12 @@ import subprocess
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
# Import the device class from the component that you want to support
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS,
|
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR,
|
||||||
ATTR_RGB_COLOR, SUPPORT_RGB_COLOR,
|
|
||||||
Light, PLATFORM_SCHEMA)
|
Light, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
|
|
||||||
# Home Assistant depends on 3rd party packages for API specific code.
|
|
||||||
REQUIREMENTS = ['piglow==1.2.4']
|
REQUIREMENTS = ['piglow==1.2.4']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -32,7 +29,7 @@ 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 Piglow Light platform."""
|
"""Set up the Piglow Light platform."""
|
||||||
import piglow
|
import piglow
|
||||||
|
|
||||||
if subprocess.getoutput("i2cdetect -q -y 1 | grep -o 54") != '54':
|
if subprocess.getoutput("i2cdetect -q -y 1 | grep -o 54") != '54':
|
||||||
|
@ -41,7 +38,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
|
|
||||||
# Add devices
|
|
||||||
add_devices([PiglowLight(piglow, name)])
|
add_devices([PiglowLight(piglow, name)])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,12 +10,14 @@ import logging
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
|
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
|
||||||
from homeassistant.components.rflink import (
|
from homeassistant.components.rflink import (
|
||||||
CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT,
|
CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICE_DEFAULTS, CONF_DEVICES,
|
||||||
CONF_IGNORE_DEVICES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER,
|
CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASSES, CONF_IGNORE_DEVICES,
|
||||||
DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA, DOMAIN,
|
CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER,
|
||||||
EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, vol)
|
DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA,
|
||||||
|
DOMAIN, EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, vol)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_PLATFORM, CONF_TYPE, STATE_UNKNOWN)
|
CONF_NAME, CONF_PLATFORM, CONF_TYPE, STATE_UNKNOWN)
|
||||||
|
|
||||||
DEPENDENCIES = ['rflink']
|
DEPENDENCIES = ['rflink']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -30,6 +32,7 @@ PLATFORM_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CONF_IGNORE_DEVICES): vol.All(cv.ensure_list, [cv.string]),
|
vol.Optional(CONF_IGNORE_DEVICES): vol.All(cv.ensure_list, [cv.string]),
|
||||||
vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})):
|
vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})):
|
||||||
DEVICE_DEFAULTS_SCHEMA,
|
DEVICE_DEFAULTS_SCHEMA,
|
||||||
|
vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean,
|
||||||
vol.Optional(CONF_DEVICES, default={}): vol.Schema({
|
vol.Optional(CONF_DEVICES, default={}): vol.Schema({
|
||||||
cv.string: {
|
cv.string: {
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
|
@ -38,8 +41,13 @@ PLATFORM_SCHEMA = vol.Schema({
|
||||||
TYPE_HYBRID, TYPE_TOGGLE),
|
TYPE_HYBRID, TYPE_TOGGLE),
|
||||||
vol.Optional(CONF_ALIASSES, default=[]):
|
vol.Optional(CONF_ALIASSES, default=[]):
|
||||||
vol.All(cv.ensure_list, [cv.string]),
|
vol.All(cv.ensure_list, [cv.string]),
|
||||||
|
vol.Optional(CONF_GROUP_ALIASSES, default=[]):
|
||||||
|
vol.All(cv.ensure_list, [cv.string]),
|
||||||
|
vol.Optional(CONF_NOGROUP_ALIASSES, default=[]):
|
||||||
|
vol.All(cv.ensure_list, [cv.string]),
|
||||||
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
|
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
|
||||||
vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int),
|
vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int),
|
||||||
|
vol.Optional(CONF_GROUP, default=True): cv.boolean,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
@ -110,7 +118,24 @@ def devices_from_config(domain_config, hass=None):
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
|
|
||||||
# Register entity (and aliasses) to listen to incoming rflink events
|
# Register entity (and aliasses) to listen to incoming rflink events
|
||||||
for _id in [device_id] + config[CONF_ALIASSES]:
|
|
||||||
|
# device id and normal aliasses respond to normal and group command
|
||||||
|
hass.data[DATA_ENTITY_LOOKUP][
|
||||||
|
EVENT_KEY_COMMAND][device_id].append(device)
|
||||||
|
if config[CONF_GROUP]:
|
||||||
|
hass.data[DATA_ENTITY_GROUP_LOOKUP][
|
||||||
|
EVENT_KEY_COMMAND][device_id].append(device)
|
||||||
|
for _id in config[CONF_ALIASSES]:
|
||||||
|
hass.data[DATA_ENTITY_LOOKUP][
|
||||||
|
EVENT_KEY_COMMAND][_id].append(device)
|
||||||
|
hass.data[DATA_ENTITY_GROUP_LOOKUP][
|
||||||
|
EVENT_KEY_COMMAND][_id].append(device)
|
||||||
|
# group_aliasses only respond to group commands
|
||||||
|
for _id in config[CONF_GROUP_ALIASSES]:
|
||||||
|
hass.data[DATA_ENTITY_GROUP_LOOKUP][
|
||||||
|
EVENT_KEY_COMMAND][_id].append(device)
|
||||||
|
# nogroup_aliasses only respond to normal commands
|
||||||
|
for _id in config[CONF_NOGROUP_ALIASSES]:
|
||||||
hass.data[DATA_ENTITY_LOOKUP][
|
hass.data[DATA_ENTITY_LOOKUP][
|
||||||
EVENT_KEY_COMMAND][_id].append(device)
|
EVENT_KEY_COMMAND][_id].append(device)
|
||||||
|
|
||||||
|
@ -138,9 +163,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
hass.data[DATA_ENTITY_LOOKUP][
|
hass.data[DATA_ENTITY_LOOKUP][
|
||||||
EVENT_KEY_COMMAND][device_id].append(device)
|
EVENT_KEY_COMMAND][device_id].append(device)
|
||||||
|
|
||||||
# Make sure the event is processed by the new entity
|
# Schedule task to process event after entity is created
|
||||||
device.handle_event(event)
|
hass.async_add_job(device.handle_event, event)
|
||||||
|
|
||||||
|
if config[CONF_AUTOMATIC_ADD]:
|
||||||
hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_COMMAND] = add_new_device
|
hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_COMMAND] = add_new_device
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ import logging
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS)
|
ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS)
|
||||||
from homeassistant.const import (STATE_OFF, STATE_ON)
|
|
||||||
from homeassistant.components.vera import (
|
from homeassistant.components.vera import (
|
||||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||||
|
|
||||||
|
@ -53,13 +52,13 @@ class VeraLight(VeraDevice, Light):
|
||||||
else:
|
else:
|
||||||
self.vera_device.switch_on()
|
self.vera_device.switch_on()
|
||||||
|
|
||||||
self._state = STATE_ON
|
self._state = True
|
||||||
self.schedule_update_ha_state(True)
|
self.schedule_update_ha_state(True)
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the light off."""
|
"""Turn the light off."""
|
||||||
self.vera_device.switch_off()
|
self.vera_device.switch_off()
|
||||||
self._state = STATE_OFF
|
self._state = False
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -44,13 +44,14 @@ DEVICE_SCHEMA = vol.Schema({
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
{vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, })
|
{vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, })
|
||||||
|
|
||||||
SUPPORT_YEELIGHT_RGB = (SUPPORT_RGB_COLOR |
|
|
||||||
SUPPORT_COLOR_TEMP)
|
|
||||||
|
|
||||||
SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS |
|
SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS |
|
||||||
SUPPORT_TRANSITION |
|
SUPPORT_TRANSITION |
|
||||||
SUPPORT_FLASH)
|
SUPPORT_FLASH)
|
||||||
|
|
||||||
|
SUPPORT_YEELIGHT_RGB = (SUPPORT_YEELIGHT |
|
||||||
|
SUPPORT_RGB_COLOR |
|
||||||
|
SUPPORT_COLOR_TEMP)
|
||||||
|
|
||||||
|
|
||||||
def _cmd(func):
|
def _cmd(func):
|
||||||
"""A wrapper to catch exceptions from the bulb."""
|
"""A wrapper to catch exceptions from the bulb."""
|
||||||
|
@ -179,9 +180,6 @@ class YeelightLight(Light):
|
||||||
self._bulb_device = yeelight.Bulb(self._ipaddr)
|
self._bulb_device = yeelight.Bulb(self._ipaddr)
|
||||||
self._bulb_device.get_properties() # force init for type
|
self._bulb_device.get_properties() # force init for type
|
||||||
|
|
||||||
btype = self._bulb_device.bulb_type
|
|
||||||
if btype == yeelight.BulbType.Color:
|
|
||||||
self._supported_features |= SUPPORT_YEELIGHT_RGB
|
|
||||||
self._available = True
|
self._available = True
|
||||||
except yeelight.BulbException as ex:
|
except yeelight.BulbException as ex:
|
||||||
self._available = False
|
self._available = False
|
||||||
|
@ -203,6 +201,9 @@ class YeelightLight(Light):
|
||||||
try:
|
try:
|
||||||
self._bulb.get_properties()
|
self._bulb.get_properties()
|
||||||
|
|
||||||
|
if self._bulb_device.bulb_type == yeelight.BulbType.Color:
|
||||||
|
self._supported_features = SUPPORT_YEELIGHT_RGB
|
||||||
|
|
||||||
self._is_on = self._properties.get("power") == "on"
|
self._is_on = self._properties.get("power") == "on"
|
||||||
|
|
||||||
bright = self._properties.get("bright", None)
|
bright = self._properties.get("bright", None)
|
||||||
|
@ -254,7 +255,8 @@ class YeelightLight(Light):
|
||||||
def set_flash(self, flash) -> None:
|
def set_flash(self, flash) -> None:
|
||||||
"""Activate flash."""
|
"""Activate flash."""
|
||||||
if flash:
|
if flash:
|
||||||
from yeelight import RGBTransition, SleepTransition, Flow
|
from yeelight import (RGBTransition, SleepTransition, Flow,
|
||||||
|
BulbException)
|
||||||
if self._bulb.last_properties["color_mode"] != 1:
|
if self._bulb.last_properties["color_mode"] != 1:
|
||||||
_LOGGER.error("Flash supported currently only in RGB mode.")
|
_LOGGER.error("Flash supported currently only in RGB mode.")
|
||||||
return
|
return
|
||||||
|
@ -279,10 +281,14 @@ class YeelightLight(Light):
|
||||||
duration=duration))
|
duration=duration))
|
||||||
|
|
||||||
flow = Flow(count=count, transitions=transitions)
|
flow = Flow(count=count, transitions=transitions)
|
||||||
|
try:
|
||||||
self._bulb.start_flow(flow)
|
self._bulb.start_flow(flow)
|
||||||
|
except BulbException as ex:
|
||||||
|
_LOGGER.error("Unable to set flash: %s", ex)
|
||||||
|
|
||||||
def turn_on(self, **kwargs) -> None:
|
def turn_on(self, **kwargs) -> None:
|
||||||
"""Turn the bulb on."""
|
"""Turn the bulb on."""
|
||||||
|
import yeelight
|
||||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||||
colortemp = kwargs.get(ATTR_COLOR_TEMP)
|
colortemp = kwargs.get(ATTR_COLOR_TEMP)
|
||||||
rgb = kwargs.get(ATTR_RGB_COLOR)
|
rgb = kwargs.get(ATTR_RGB_COLOR)
|
||||||
|
@ -292,22 +298,43 @@ class YeelightLight(Light):
|
||||||
if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
|
if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
|
||||||
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
|
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
|
||||||
|
|
||||||
|
try:
|
||||||
self._bulb.turn_on(duration=duration)
|
self._bulb.turn_on(duration=duration)
|
||||||
|
except yeelight.BulbException as ex:
|
||||||
|
_LOGGER.error("Unable to turn the bulb on: %s", ex)
|
||||||
|
return
|
||||||
|
|
||||||
if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode:
|
if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode:
|
||||||
|
try:
|
||||||
self.set_music_mode(self.config[CONF_MODE_MUSIC])
|
self.set_music_mode(self.config[CONF_MODE_MUSIC])
|
||||||
|
except yeelight.BulbException as ex:
|
||||||
|
_LOGGER.error("Unable to turn on music mode,"
|
||||||
|
"consider disabling it: %s", ex)
|
||||||
|
|
||||||
|
try:
|
||||||
# values checked for none in methods
|
# values checked for none in methods
|
||||||
self.set_rgb(rgb, duration)
|
self.set_rgb(rgb, duration)
|
||||||
self.set_colortemp(colortemp, duration)
|
self.set_colortemp(colortemp, duration)
|
||||||
self.set_brightness(brightness, duration)
|
self.set_brightness(brightness, duration)
|
||||||
self.set_flash(flash)
|
self.set_flash(flash)
|
||||||
|
except yeelight.BulbException as ex:
|
||||||
|
_LOGGER.error("Unable to set bulb properties: %s", ex)
|
||||||
|
return
|
||||||
|
|
||||||
# save the current state if we had a manual change.
|
# save the current state if we had a manual change.
|
||||||
if self.config[CONF_SAVE_ON_CHANGE]:
|
if self.config[CONF_SAVE_ON_CHANGE] and (brightness
|
||||||
if brightness or colortemp or rgb:
|
or colortemp
|
||||||
|
or rgb):
|
||||||
|
try:
|
||||||
self.set_default()
|
self.set_default()
|
||||||
|
except yeelight.BulbException as ex:
|
||||||
|
_LOGGER.error("Unable to set the defaults: %s", ex)
|
||||||
|
return
|
||||||
|
|
||||||
def turn_off(self, **kwargs) -> None:
|
def turn_off(self, **kwargs) -> None:
|
||||||
"""Turn off."""
|
"""Turn off."""
|
||||||
|
import yeelight
|
||||||
|
try:
|
||||||
self._bulb.turn_off()
|
self._bulb.turn_off()
|
||||||
|
except yeelight.BulbException as ex:
|
||||||
|
_LOGGER.error("Unable to turn the bulb off: %s", ex)
|
||||||
|
|
|
@ -5,21 +5,19 @@ For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/light.yeelightsunflower/
|
https://home-assistant.io/components/light.yeelightsunflower/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.light import (Light,
|
|
||||||
ATTR_RGB_COLOR, SUPPORT_RGB_COLOR,
|
|
||||||
ATTR_BRIGHTNESS,
|
|
||||||
SUPPORT_BRIGHTNESS,
|
|
||||||
PLATFORM_SCHEMA)
|
|
||||||
from homeassistant.const import CONF_HOST
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.light import (
|
||||||
|
Light, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, ATTR_BRIGHTNESS,
|
||||||
|
SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA)
|
||||||
|
from homeassistant.const import CONF_HOST
|
||||||
|
|
||||||
REQUIREMENTS = ['yeelightsunflower==0.0.8']
|
REQUIREMENTS = ['yeelightsunflower==0.0.8']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR)
|
SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
@ -35,7 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
hub = yeelightsunflower.Hub(host)
|
hub = yeelightsunflower.Hub(host)
|
||||||
|
|
||||||
if not hub.available:
|
if not hub.available:
|
||||||
_LOGGER.error('Could not connect to Yeelight Sunflower hub')
|
_LOGGER.error("Could not connect to Yeelight Sunflower hub")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
add_devices(SunflowerBulb(light) for light in hub.get_lights())
|
add_devices(SunflowerBulb(light) for light in hub.get_lights())
|
||||||
|
@ -55,7 +53,7 @@ class SunflowerBulb(Light):
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the display name of this light."""
|
"""Return the display name of this light."""
|
||||||
return "sunflower_{}".format(self._light.zid)
|
return 'sunflower_{}'.format(self._light.zid)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
|
|
|
@ -10,8 +10,8 @@ import logging
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
from threading import Timer
|
from threading import Timer
|
||||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
|
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
|
||||||
ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \
|
ATTR_RGB_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \
|
||||||
SUPPORT_RGB_COLOR, DOMAIN, Light
|
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, DOMAIN, Light
|
||||||
from homeassistant.components import zwave
|
from homeassistant.components import zwave
|
||||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
|
@ -43,11 +43,6 @@ TEMP_MID_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 2 + HASS_COLOR_MIN
|
||||||
TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN
|
TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN
|
||||||
TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN
|
TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN
|
||||||
|
|
||||||
SUPPORT_ZWAVE_DIMMER = SUPPORT_BRIGHTNESS
|
|
||||||
SUPPORT_ZWAVE_COLOR = SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
|
|
||||||
SUPPORT_ZWAVE_COLORTEMP = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
|
|
||||||
| SUPPORT_COLOR_TEMP)
|
|
||||||
|
|
||||||
|
|
||||||
def get_device(node, values, node_config, **kwargs):
|
def get_device(node, values, node_config, **kwargs):
|
||||||
"""Create zwave entity device."""
|
"""Create zwave entity device."""
|
||||||
|
@ -72,6 +67,13 @@ def brightness_state(value):
|
||||||
return 0, STATE_OFF
|
return 0, STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
def ct_to_rgb(temp):
|
||||||
|
"""Convert color temperature (mireds) to RGB."""
|
||||||
|
colorlist = list(
|
||||||
|
color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp)))
|
||||||
|
return [int(val) for val in colorlist]
|
||||||
|
|
||||||
|
|
||||||
class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||||
"""Representation of a Z-Wave dimmer."""
|
"""Representation of a Z-Wave dimmer."""
|
||||||
|
|
||||||
|
@ -80,6 +82,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||||
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||||
self._brightness = None
|
self._brightness = None
|
||||||
self._state = None
|
self._state = None
|
||||||
|
self._supported_features = None
|
||||||
self._delay = delay
|
self._delay = delay
|
||||||
self._refresh_value = refresh
|
self._refresh_value = refresh
|
||||||
self._zw098 = None
|
self._zw098 = None
|
||||||
|
@ -100,6 +103,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||||
self._timer = None
|
self._timer = None
|
||||||
_LOGGER.debug('self._refreshing=%s self.delay=%s',
|
_LOGGER.debug('self._refreshing=%s self.delay=%s',
|
||||||
self._refresh_value, self._delay)
|
self._refresh_value, self._delay)
|
||||||
|
self.value_added()
|
||||||
self.update_properties()
|
self.update_properties()
|
||||||
|
|
||||||
def update_properties(self):
|
def update_properties(self):
|
||||||
|
@ -107,6 +111,12 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||||
# Brightness
|
# Brightness
|
||||||
self._brightness, self._state = brightness_state(self.values.primary)
|
self._brightness, self._state = brightness_state(self.values.primary)
|
||||||
|
|
||||||
|
def value_added(self):
|
||||||
|
"""Called when a new value is added to this entity."""
|
||||||
|
self._supported_features = SUPPORT_BRIGHTNESS
|
||||||
|
if self.values.dimming_duration is not None:
|
||||||
|
self._supported_features |= SUPPORT_TRANSITION
|
||||||
|
|
||||||
def value_changed(self):
|
def value_changed(self):
|
||||||
"""Called when a value for this entity's node has changed."""
|
"""Called when a value for this entity's node has changed."""
|
||||||
if self._refresh_value:
|
if self._refresh_value:
|
||||||
|
@ -139,10 +149,43 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Flag supported features."""
|
"""Flag supported features."""
|
||||||
return SUPPORT_ZWAVE_DIMMER
|
return self._supported_features
|
||||||
|
|
||||||
|
def _set_duration(self, **kwargs):
|
||||||
|
"""Set the transition time for the brightness value.
|
||||||
|
|
||||||
|
Zwave Dimming Duration values:
|
||||||
|
0x00 = instant
|
||||||
|
0x01-0x7F = 1 second to 127 seconds
|
||||||
|
0x80-0xFE = 1 minute to 127 minutes
|
||||||
|
0xFF = factory default
|
||||||
|
"""
|
||||||
|
if self.values.dimming_duration is None:
|
||||||
|
if ATTR_TRANSITION in kwargs:
|
||||||
|
_LOGGER.debug("Dimming not supported by %s.", self.entity_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
if ATTR_TRANSITION not in kwargs:
|
||||||
|
self.values.dimming_duration.data = 0xFF
|
||||||
|
return
|
||||||
|
|
||||||
|
transition = kwargs[ATTR_TRANSITION]
|
||||||
|
if transition <= 127:
|
||||||
|
self.values.dimming_duration.data = int(transition)
|
||||||
|
elif transition > 7620:
|
||||||
|
self.values.dimming_duration.data = 0xFE
|
||||||
|
_LOGGER.warning("Transition clipped to 127 minutes for %s.",
|
||||||
|
self.entity_id)
|
||||||
|
else:
|
||||||
|
minutes = int(transition / 60)
|
||||||
|
_LOGGER.debug("Transition rounded to %d minutes for %s.",
|
||||||
|
minutes, self.entity_id)
|
||||||
|
self.values.dimming_duration.data = minutes + 0x7F
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
|
self._set_duration(**kwargs)
|
||||||
|
|
||||||
# Zwave multilevel switches use a range of [0, 99] to control
|
# Zwave multilevel switches use a range of [0, 99] to control
|
||||||
# brightness. Level 255 means to set it to previous value.
|
# brightness. Level 255 means to set it to previous value.
|
||||||
if ATTR_BRIGHTNESS in kwargs:
|
if ATTR_BRIGHTNESS in kwargs:
|
||||||
|
@ -156,17 +199,12 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
|
self._set_duration(**kwargs)
|
||||||
|
|
||||||
if self.node.set_dimmer(self.values.primary.value_id, 0):
|
if self.node.set_dimmer(self.values.primary.value_id, 0):
|
||||||
self._state = STATE_OFF
|
self._state = STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
def ct_to_rgb(temp):
|
|
||||||
"""Convert color temperature (mireds) to RGB."""
|
|
||||||
colorlist = list(
|
|
||||||
color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp)))
|
|
||||||
return [int(val) for val in colorlist]
|
|
||||||
|
|
||||||
|
|
||||||
class ZwaveColorLight(ZwaveDimmer):
|
class ZwaveColorLight(ZwaveDimmer):
|
||||||
"""Representation of a Z-Wave color changing light."""
|
"""Representation of a Z-Wave color changing light."""
|
||||||
|
|
||||||
|
@ -178,6 +216,14 @@ class ZwaveColorLight(ZwaveDimmer):
|
||||||
|
|
||||||
super().__init__(values, refresh, delay)
|
super().__init__(values, refresh, delay)
|
||||||
|
|
||||||
|
def value_added(self):
|
||||||
|
"""Called when a new value is added to this entity."""
|
||||||
|
super().value_added()
|
||||||
|
|
||||||
|
self._supported_features |= SUPPORT_RGB_COLOR
|
||||||
|
if self._zw098:
|
||||||
|
self._supported_features |= SUPPORT_COLOR_TEMP
|
||||||
|
|
||||||
def update_properties(self):
|
def update_properties(self):
|
||||||
"""Update internal properties based on zwave values."""
|
"""Update internal properties based on zwave values."""
|
||||||
super().update_properties()
|
super().update_properties()
|
||||||
|
@ -288,11 +334,3 @@ class ZwaveColorLight(ZwaveDimmer):
|
||||||
self.values.color.data = rgbw
|
self.values.color.data = rgbw
|
||||||
|
|
||||||
super().turn_on(**kwargs)
|
super().turn_on(**kwargs)
|
||||||
|
|
||||||
@property
|
|
||||||
def supported_features(self):
|
|
||||||
"""Flag supported features."""
|
|
||||||
if self._zw098:
|
|
||||||
return SUPPORT_ZWAVE_COLORTEMP
|
|
||||||
else:
|
|
||||||
return SUPPORT_ZWAVE_COLOR
|
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
"""
|
||||||
|
Lockitron lock platform.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation
|
||||||
|
https://home-assistant.io/components/lockitron/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ID
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DOMAIN = 'lockitron'
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_ACCESS_TOKEN): cv.string,
|
||||||
|
vol.Required(CONF_ID): cv.string
|
||||||
|
})
|
||||||
|
BASE_URL = 'https://api.lockitron.com'
|
||||||
|
API_STATE_URL = BASE_URL + '/v2/locks/{}?access_token={}'
|
||||||
|
API_ACTION_URL = BASE_URL + '/v2/locks/{}?access_token={}&state={}'
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the Lockitron platform."""
|
||||||
|
access_token = config.get(CONF_ACCESS_TOKEN)
|
||||||
|
device_id = config.get(CONF_ID)
|
||||||
|
response = requests.get(API_STATE_URL.format(device_id, access_token))
|
||||||
|
if response.status_code == 200:
|
||||||
|
add_devices([Lockitron(response.json()['state'], access_token,
|
||||||
|
device_id)])
|
||||||
|
else:
|
||||||
|
_LOGGER.error('Error retrieving lock status during init: %s',
|
||||||
|
response.text)
|
||||||
|
|
||||||
|
|
||||||
|
class Lockitron(LockDevice):
|
||||||
|
"""Representation of a Lockitron lock."""
|
||||||
|
|
||||||
|
LOCK_STATE = 'lock'
|
||||||
|
UNLOCK_STATE = 'unlock'
|
||||||
|
|
||||||
|
def __init__(self, state, access_token, device_id):
|
||||||
|
"""Initialize the lock."""
|
||||||
|
self._state = state
|
||||||
|
self.access_token = access_token
|
||||||
|
self.device_id = device_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return DOMAIN
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_locked(self):
|
||||||
|
"""Return True if the lock is currently locked, else False."""
|
||||||
|
return self._state == Lockitron.LOCK_STATE
|
||||||
|
|
||||||
|
def lock(self, **kwargs):
|
||||||
|
"""Lock the device."""
|
||||||
|
self._state = self.do_change_request(Lockitron.LOCK_STATE)
|
||||||
|
|
||||||
|
def unlock(self, **kwargs):
|
||||||
|
"""Unlock the device."""
|
||||||
|
self._state = self.do_change_request(Lockitron.UNLOCK_STATE)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Update the internal state of the device."""
|
||||||
|
response = requests \
|
||||||
|
.get(API_STATE_URL.format(self.device_id, self.access_token))
|
||||||
|
if response.status_code == 200:
|
||||||
|
self._state = response.json()['state']
|
||||||
|
else:
|
||||||
|
_LOGGER.error('Error retrieving lock status: %s', response.text)
|
||||||
|
|
||||||
|
def do_change_request(self, requested_state):
|
||||||
|
"""Execute the change request and pull out the new state."""
|
||||||
|
response = requests.put(
|
||||||
|
API_ACTION_URL.format(self.device_id, self.access_token,
|
||||||
|
requested_state))
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()['state']
|
||||||
|
else:
|
||||||
|
_LOGGER.error('Error setting lock state: %s\n%s',
|
||||||
|
requested_state, response.text)
|
||||||
|
return self._state
|
|
@ -53,8 +53,8 @@ LOCK_ALARM_TYPE = {
|
||||||
'9': 'Deadbolt Jammed',
|
'9': 'Deadbolt Jammed',
|
||||||
'18': 'Locked with Keypad by user ',
|
'18': 'Locked with Keypad by user ',
|
||||||
'19': 'Unlocked with Keypad by user ',
|
'19': 'Unlocked with Keypad by user ',
|
||||||
'21': 'Manually Locked by ',
|
'21': 'Manually Locked ',
|
||||||
'22': 'Manually Unlocked by Key or Inside thumb turn',
|
'22': 'Manually Unlocked ',
|
||||||
'24': 'Locked by RF',
|
'24': 'Locked by RF',
|
||||||
'25': 'Unlocked by RF',
|
'25': 'Unlocked by RF',
|
||||||
'27': 'Auto re-lock',
|
'27': 'Auto re-lock',
|
||||||
|
@ -69,8 +69,8 @@ LOCK_ALARM_TYPE = {
|
||||||
}
|
}
|
||||||
|
|
||||||
MANUAL_LOCK_ALARM_LEVEL = {
|
MANUAL_LOCK_ALARM_LEVEL = {
|
||||||
'1': 'Key Cylinder or Inside thumb turn',
|
'1': 'by Key Cylinder or Inside thumb turn',
|
||||||
'2': 'Touch function (lock and leave)'
|
'2': 'by Touch function (lock and leave)'
|
||||||
}
|
}
|
||||||
|
|
||||||
TAMPER_ALARM_LEVEL = {
|
TAMPER_ALARM_LEVEL = {
|
||||||
|
|
|
@ -17,8 +17,8 @@ from homeassistant.helpers import discovery
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/gurumitts/'
|
REQUIREMENTS = ['https://github.com/gurumitts/'
|
||||||
'pylutron-caseta/archive/v0.2.4.zip#'
|
'pylutron-caseta/archive/v0.2.5.zip#'
|
||||||
'pylutron-caseta==v0.2.4']
|
'pylutron-caseta==v0.2.5']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -864,10 +864,10 @@ def _async_fetch_image(hass, url):
|
||||||
|
|
||||||
content, content_type = (None, None)
|
content, content_type = (None, None)
|
||||||
websession = async_get_clientsession(hass)
|
websession = async_get_clientsession(hass)
|
||||||
response = None
|
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(10, loop=hass.loop):
|
with async_timeout.timeout(10, loop=hass.loop):
|
||||||
response = yield from websession.get(url)
|
response = yield from websession.get(url)
|
||||||
|
|
||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
content = yield from response.read()
|
content = yield from response.read()
|
||||||
content_type = response.headers.get(CONTENT_TYPE_HEADER)
|
content_type = response.headers.get(CONTENT_TYPE_HEADER)
|
||||||
|
@ -875,10 +875,6 @@ def _async_fetch_image(hass, url):
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
finally:
|
|
||||||
if response is not None:
|
|
||||||
yield from response.release()
|
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ from homeassistant.core import callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
REQUIREMENTS = ['pyemby==1.1']
|
REQUIREMENTS = ['pyemby==1.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Frontier Silicon platform."""
|
"""Set up the Frontier Silicon platform."""
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
if discovery_info is not None:
|
if discovery_info is not None:
|
||||||
|
@ -59,10 +59,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
add_devices(
|
add_devices(
|
||||||
[FSAPIDevice(DEVICE_URL.format(host, port), password)],
|
[FSAPIDevice(DEVICE_URL.format(host, port), password)],
|
||||||
update_before_add=True)
|
update_before_add=True)
|
||||||
_LOGGER.debug('FSAPI device %s:%s -> %s', host, port, password)
|
_LOGGER.debug("FSAPI device %s:%s -> %s", host, port, password)
|
||||||
return True
|
return True
|
||||||
except requests.exceptions.RequestException:
|
except requests.exceptions.RequestException:
|
||||||
_LOGGER.error('Could not add the FSAPI device at %s:%s -> %s',
|
_LOGGER.error("Could not add the FSAPI device at %s:%s -> %s",
|
||||||
host, port, password)
|
host, port, password)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -27,7 +27,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.deprecation import get_deprecated
|
from homeassistant.helpers.deprecation import get_deprecated
|
||||||
|
|
||||||
REQUIREMENTS = ['jsonrpc-async==0.4', 'jsonrpc-websocket==0.3']
|
REQUIREMENTS = ['jsonrpc-async==0.6', 'jsonrpc-websocket==0.5']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,9 @@ from homeassistant.components.media_player import (
|
||||||
from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME)
|
from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/danieljkemp/onkyo-eiscp/archive/'
|
REQUIREMENTS = ['https://github.com/miracle2k/onkyo-eiscp/archive/'
|
||||||
'python3.zip#onkyo-eiscp==0.9.2']
|
'066023aec04770518d494c32fb72eea0ec5c1b7c.zip#'
|
||||||
|
'onkyo-eiscp==1.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -127,7 +128,7 @@ class OnkyoDevice(MediaPlayerDevice):
|
||||||
self._current_source = '_'.join(
|
self._current_source = '_'.join(
|
||||||
[i for i in current_source_tuples[1]])
|
[i for i in current_source_tuples[1]])
|
||||||
self._muted = bool(mute_raw[1] == 'on')
|
self._muted = bool(mute_raw[1] == 'on')
|
||||||
self._volume = int(volume_raw[1], 16) / 80.0
|
self._volume = volume_raw[1] / 80.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
@ -4,11 +4,14 @@ Support to interface with Sonos players (via SoCo).
|
||||||
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/media_player.sonos/
|
https://home-assistant.io/components/media_player.sonos/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
|
import functools as ft
|
||||||
import logging
|
import logging
|
||||||
from os import path
|
from os import path
|
||||||
import socket
|
import socket
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
|
@ -107,7 +110,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
return
|
return
|
||||||
|
|
||||||
if player.is_visible:
|
if player.is_visible:
|
||||||
device = SonosDevice(hass, player)
|
device = SonosDevice(player)
|
||||||
add_devices([device], True)
|
add_devices([device], True)
|
||||||
hass.data[DATA_SONOS].append(device)
|
hass.data[DATA_SONOS].append(device)
|
||||||
if len(hass.data[DATA_SONOS]) > 1:
|
if len(hass.data[DATA_SONOS]) > 1:
|
||||||
|
@ -132,7 +135,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
_LOGGER.warning('No Sonos speakers found.')
|
_LOGGER.warning('No Sonos speakers found.')
|
||||||
return
|
return
|
||||||
|
|
||||||
hass.data[DATA_SONOS] = [SonosDevice(hass, p) for p in players]
|
hass.data[DATA_SONOS] = [SonosDevice(p) for p in players]
|
||||||
add_devices(hass.data[DATA_SONOS], True)
|
add_devices(hass.data[DATA_SONOS], True)
|
||||||
_LOGGER.info('Added %s Sonos speakers', len(players))
|
_LOGGER.info('Added %s Sonos speakers', len(players))
|
||||||
|
|
||||||
|
@ -216,19 +219,42 @@ class _ProcessSonosEventQueue():
|
||||||
def _get_entity_from_soco(hass, soco):
|
def _get_entity_from_soco(hass, soco):
|
||||||
"""Return SonosDevice from SoCo."""
|
"""Return SonosDevice from SoCo."""
|
||||||
for device in hass.data[DATA_SONOS]:
|
for device in hass.data[DATA_SONOS]:
|
||||||
if soco == device.soco_device:
|
if soco == device.soco:
|
||||||
return device
|
return device
|
||||||
raise ValueError("No entity for SoCo device!")
|
raise ValueError("No entity for SoCo device!")
|
||||||
|
|
||||||
|
|
||||||
|
def soco_error(funct):
|
||||||
|
"""Decorator to catch soco exceptions."""
|
||||||
|
@ft.wraps(funct)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
"""Wrapper for all soco exception."""
|
||||||
|
from soco.exceptions import SoCoException
|
||||||
|
try:
|
||||||
|
return funct(*args, **kwargs)
|
||||||
|
except SoCoException as err:
|
||||||
|
_LOGGER.error("Error on %s with %s.", funct.__name__, err)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def soco_coordinator(funct):
|
||||||
|
"""Decorator to call funct on coordinator."""
|
||||||
|
@ft.wraps(funct)
|
||||||
|
def wrapper(device, *args, **kwargs):
|
||||||
|
"""Wrapper for call to coordinator."""
|
||||||
|
if device.is_coordinator:
|
||||||
|
return funct(device, *args, **kwargs)
|
||||||
|
return funct(device.coordinator, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
class SonosDevice(MediaPlayerDevice):
|
class SonosDevice(MediaPlayerDevice):
|
||||||
"""Representation of a Sonos device."""
|
"""Representation of a Sonos device."""
|
||||||
|
|
||||||
def __init__(self, hass, player):
|
def __init__(self, player):
|
||||||
"""Initialize the Sonos device."""
|
"""Initialize the Sonos device."""
|
||||||
from soco.snapshot import Snapshot
|
|
||||||
|
|
||||||
self.hass = hass
|
|
||||||
self.volume_increment = 5
|
self.volume_increment = 5
|
||||||
self._unique_id = player.uid
|
self._unique_id = player.uid
|
||||||
self._player = player
|
self._player = player
|
||||||
|
@ -260,9 +286,14 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
self._is_playing_tv = None
|
self._is_playing_tv = None
|
||||||
self._favorite_sources = None
|
self._favorite_sources = None
|
||||||
self._source_name = None
|
self._source_name = None
|
||||||
self.soco_snapshot = Snapshot(self._player)
|
self._soco_snapshot = None
|
||||||
self._snapshot_group = None
|
self._snapshot_group = None
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_added_to_hass(self):
|
||||||
|
"""Subscribe sonos events."""
|
||||||
|
self.hass.loop.run_in_executor(None, self._subscribe_to_player_events)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
"""Polling needed."""
|
"""Polling needed."""
|
||||||
|
@ -297,7 +328,7 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
return self._coordinator is None
|
return self._coordinator is None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def soco_device(self):
|
def soco(self):
|
||||||
"""Return soco device."""
|
"""Return soco device."""
|
||||||
return self._player
|
return self._player
|
||||||
|
|
||||||
|
@ -327,7 +358,6 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
auto_renew=True,
|
auto_renew=True,
|
||||||
event_queue=self._queue)
|
event_queue=self._queue)
|
||||||
|
|
||||||
# pylint: disable=too-many-branches, too-many-statements
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Retrieve latest state."""
|
"""Retrieve latest state."""
|
||||||
if self._speaker_info is None:
|
if self._speaker_info is None:
|
||||||
|
@ -606,16 +636,6 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
self._is_playing_tv = is_playing_tv
|
self._is_playing_tv = is_playing_tv
|
||||||
self._is_playing_line_in = is_playing_line_in
|
self._is_playing_line_in = is_playing_line_in
|
||||||
self._source_name = source_name
|
self._source_name = source_name
|
||||||
|
|
||||||
# update state of the whole group
|
|
||||||
for device in [x for x in self.hass.data[DATA_SONOS]
|
|
||||||
if x.coordinator == self]:
|
|
||||||
if device.entity_id is not self.entity_id:
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
|
|
||||||
if self._queue is None and self.entity_id is not None:
|
|
||||||
self._subscribe_to_player_events()
|
|
||||||
|
|
||||||
self._last_avtransport_event = None
|
self._last_avtransport_event = None
|
||||||
|
|
||||||
def _format_media_image_url(self, url, fallback_uri):
|
def _format_media_image_url(self, url, fallback_uri):
|
||||||
|
@ -781,27 +801,31 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
|
|
||||||
return supported
|
return supported
|
||||||
|
|
||||||
|
@soco_error
|
||||||
def volume_up(self):
|
def volume_up(self):
|
||||||
"""Volume up media player."""
|
"""Volume up media player."""
|
||||||
self._player.volume += self.volume_increment
|
self._player.volume += self.volume_increment
|
||||||
|
|
||||||
|
@soco_error
|
||||||
def volume_down(self):
|
def volume_down(self):
|
||||||
"""Volume down media player."""
|
"""Volume down media player."""
|
||||||
self._player.volume -= self.volume_increment
|
self._player.volume -= self.volume_increment
|
||||||
|
|
||||||
|
@soco_error
|
||||||
def set_volume_level(self, volume):
|
def set_volume_level(self, volume):
|
||||||
"""Set volume level, range 0..1."""
|
"""Set volume level, range 0..1."""
|
||||||
self._player.volume = str(int(volume * 100))
|
self._player.volume = str(int(volume * 100))
|
||||||
|
|
||||||
|
@soco_error
|
||||||
def mute_volume(self, mute):
|
def mute_volume(self, mute):
|
||||||
"""Mute (true) or unmute (false) media player."""
|
"""Mute (true) or unmute (false) media player."""
|
||||||
self._player.mute = mute
|
self._player.mute = mute
|
||||||
|
|
||||||
|
@soco_error
|
||||||
|
@soco_coordinator
|
||||||
def select_source(self, source):
|
def select_source(self, source):
|
||||||
"""Select input source."""
|
"""Select input source."""
|
||||||
if self._coordinator:
|
if source == SUPPORT_SOURCE_LINEIN:
|
||||||
self._coordinator.select_source(source)
|
|
||||||
elif source == SUPPORT_SOURCE_LINEIN:
|
|
||||||
self._source_name = SUPPORT_SOURCE_LINEIN
|
self._source_name = SUPPORT_SOURCE_LINEIN
|
||||||
self._player.switch_to_line_in()
|
self._player.switch_to_line_in()
|
||||||
elif source == SUPPORT_SOURCE_TV:
|
elif source == SUPPORT_SOURCE_TV:
|
||||||
|
@ -842,72 +866,66 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
else:
|
else:
|
||||||
return self._source_name
|
return self._source_name
|
||||||
|
|
||||||
|
@soco_error
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
"""Turn off media player."""
|
"""Turn off media player."""
|
||||||
self.media_pause()
|
self.media_pause()
|
||||||
|
|
||||||
|
@soco_error
|
||||||
|
@soco_coordinator
|
||||||
def media_play(self):
|
def media_play(self):
|
||||||
"""Send play command."""
|
"""Send play command."""
|
||||||
if self._coordinator:
|
|
||||||
self._coordinator.media_play()
|
|
||||||
else:
|
|
||||||
self._player.play()
|
self._player.play()
|
||||||
|
|
||||||
|
@soco_error
|
||||||
|
@soco_coordinator
|
||||||
def media_stop(self):
|
def media_stop(self):
|
||||||
"""Send stop command."""
|
"""Send stop command."""
|
||||||
if self._coordinator:
|
|
||||||
self._coordinator.media_stop()
|
|
||||||
else:
|
|
||||||
self._player.stop()
|
self._player.stop()
|
||||||
|
|
||||||
|
@soco_error
|
||||||
|
@soco_coordinator
|
||||||
def media_pause(self):
|
def media_pause(self):
|
||||||
"""Send pause command."""
|
"""Send pause command."""
|
||||||
if self._coordinator:
|
|
||||||
self._coordinator.media_pause()
|
|
||||||
else:
|
|
||||||
self._player.pause()
|
self._player.pause()
|
||||||
|
|
||||||
|
@soco_error
|
||||||
|
@soco_coordinator
|
||||||
def media_next_track(self):
|
def media_next_track(self):
|
||||||
"""Send next track command."""
|
"""Send next track command."""
|
||||||
if self._coordinator:
|
|
||||||
self._coordinator.media_next_track()
|
|
||||||
else:
|
|
||||||
self._player.next()
|
self._player.next()
|
||||||
|
|
||||||
|
@soco_error
|
||||||
|
@soco_coordinator
|
||||||
def media_previous_track(self):
|
def media_previous_track(self):
|
||||||
"""Send next track command."""
|
"""Send next track command."""
|
||||||
if self._coordinator:
|
|
||||||
self._coordinator.media_previous_track()
|
|
||||||
else:
|
|
||||||
self._player.previous()
|
self._player.previous()
|
||||||
|
|
||||||
|
@soco_error
|
||||||
|
@soco_coordinator
|
||||||
def media_seek(self, position):
|
def media_seek(self, position):
|
||||||
"""Send seek command."""
|
"""Send seek command."""
|
||||||
if self._coordinator:
|
|
||||||
self._coordinator.media_seek(position)
|
|
||||||
else:
|
|
||||||
self._player.seek(str(datetime.timedelta(seconds=int(position))))
|
self._player.seek(str(datetime.timedelta(seconds=int(position))))
|
||||||
|
|
||||||
|
@soco_error
|
||||||
|
@soco_coordinator
|
||||||
def clear_playlist(self):
|
def clear_playlist(self):
|
||||||
"""Clear players playlist."""
|
"""Clear players playlist."""
|
||||||
if self._coordinator:
|
|
||||||
self._coordinator.clear_playlist()
|
|
||||||
else:
|
|
||||||
self._player.clear_queue()
|
self._player.clear_queue()
|
||||||
|
|
||||||
|
@soco_error
|
||||||
def turn_on(self):
|
def turn_on(self):
|
||||||
"""Turn the media player on."""
|
"""Turn the media player on."""
|
||||||
self.media_play()
|
self.media_play()
|
||||||
|
|
||||||
|
@soco_error
|
||||||
|
@soco_coordinator
|
||||||
def play_media(self, media_type, media_id, **kwargs):
|
def play_media(self, media_type, media_id, **kwargs):
|
||||||
"""
|
"""
|
||||||
Send the play_media command to the media player.
|
Send the play_media command to the media player.
|
||||||
|
|
||||||
If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
|
If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
|
||||||
"""
|
"""
|
||||||
if self._coordinator:
|
|
||||||
self._coordinator.play_media(media_type, media_id, **kwargs)
|
|
||||||
else:
|
|
||||||
if kwargs.get(ATTR_MEDIA_ENQUEUE):
|
if kwargs.get(ATTR_MEDIA_ENQUEUE):
|
||||||
from soco.exceptions import SoCoUPnPException
|
from soco.exceptions import SoCoUPnPException
|
||||||
try:
|
try:
|
||||||
|
@ -919,6 +937,7 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
else:
|
else:
|
||||||
self._player.play_uri(media_id)
|
self._player.play_uri(media_id)
|
||||||
|
|
||||||
|
@soco_error
|
||||||
def join(self, master):
|
def join(self, master):
|
||||||
"""Join the player to a group."""
|
"""Join the player to a group."""
|
||||||
coord = [device for device in self.hass.data[DATA_SONOS]
|
coord = [device for device in self.hass.data[DATA_SONOS]
|
||||||
|
@ -926,29 +945,26 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
|
|
||||||
if coord and master != self.entity_id:
|
if coord and master != self.entity_id:
|
||||||
coord = coord[0]
|
coord = coord[0]
|
||||||
if coord.soco_device.group.coordinator != coord.soco_device:
|
if coord.soco.group.coordinator != coord.soco:
|
||||||
coord.soco_device.unjoin()
|
coord.soco.unjoin()
|
||||||
self._player.join(coord.soco_device)
|
self._player.join(coord.soco)
|
||||||
self._coordinator = coord
|
self._coordinator = coord
|
||||||
else:
|
else:
|
||||||
_LOGGER.error("Master not found %s", master)
|
_LOGGER.error("Master not found %s", master)
|
||||||
|
|
||||||
|
@soco_error
|
||||||
def unjoin(self):
|
def unjoin(self):
|
||||||
"""Unjoin the player from a group."""
|
"""Unjoin the player from a group."""
|
||||||
self._player.unjoin()
|
self._player.unjoin()
|
||||||
self._coordinator = None
|
self._coordinator = None
|
||||||
|
|
||||||
|
@soco_error
|
||||||
def snapshot(self, with_group=True):
|
def snapshot(self, with_group=True):
|
||||||
"""Snapshot the player."""
|
"""Snapshot the player."""
|
||||||
from soco.exceptions import SoCoException
|
from soco.snapshot import Snapshot
|
||||||
try:
|
|
||||||
self.soco_snapshot.is_playing_queue = False
|
self._soco_snapshot = Snapshot(self._player)
|
||||||
self.soco_snapshot.is_coordinator = False
|
self._soco_snapshot.snapshot()
|
||||||
self.soco_snapshot.snapshot()
|
|
||||||
except SoCoException:
|
|
||||||
_LOGGER.debug("Error on snapshot %s", self.entity_id)
|
|
||||||
self._snapshot_group = None
|
|
||||||
return
|
|
||||||
|
|
||||||
if with_group:
|
if with_group:
|
||||||
self._snapshot_group = self._player.group
|
self._snapshot_group = self._player.group
|
||||||
|
@ -957,14 +973,15 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
else:
|
else:
|
||||||
self._snapshot_group = None
|
self._snapshot_group = None
|
||||||
|
|
||||||
|
@soco_error
|
||||||
def restore(self, with_group=True):
|
def restore(self, with_group=True):
|
||||||
"""Restore snapshot for the player."""
|
"""Restore snapshot for the player."""
|
||||||
from soco.exceptions import SoCoException
|
from soco.exceptions import SoCoException
|
||||||
try:
|
try:
|
||||||
# need catch exception if a coordinator is going to slave.
|
# need catch exception if a coordinator is going to slave.
|
||||||
# this state will recover with group part.
|
# this state will recover with group part.
|
||||||
self.soco_snapshot.restore(False)
|
self._soco_snapshot.restore(False)
|
||||||
except (TypeError, SoCoException):
|
except (TypeError, AttributeError, SoCoException):
|
||||||
_LOGGER.debug("Error on restore %s", self.entity_id)
|
_LOGGER.debug("Error on restore %s", self.entity_id)
|
||||||
|
|
||||||
# restore groups
|
# restore groups
|
||||||
|
@ -1006,18 +1023,16 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
if s_dev != old.coordinator:
|
if s_dev != old.coordinator:
|
||||||
s_dev.join(old.coordinator)
|
s_dev.join(old.coordinator)
|
||||||
|
|
||||||
|
@soco_error
|
||||||
|
@soco_coordinator
|
||||||
def set_sleep_timer(self, sleep_time):
|
def set_sleep_timer(self, sleep_time):
|
||||||
"""Set the timer on the player."""
|
"""Set the timer on the player."""
|
||||||
if self._coordinator:
|
|
||||||
self._coordinator.set_sleep_timer(sleep_time)
|
|
||||||
else:
|
|
||||||
self._player.set_sleep_timer(sleep_time)
|
self._player.set_sleep_timer(sleep_time)
|
||||||
|
|
||||||
|
@soco_error
|
||||||
|
@soco_coordinator
|
||||||
def clear_sleep_timer(self):
|
def clear_sleep_timer(self):
|
||||||
"""Clear the timer on the player."""
|
"""Clear the timer on the player."""
|
||||||
if self._coordinator:
|
|
||||||
self._coordinator.set_sleep_timer(None)
|
|
||||||
else:
|
|
||||||
self._player.set_sleep_timer(None)
|
self._player.set_sleep_timer(None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -117,7 +117,6 @@ class LogitechMediaServer(object):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_query(self, *command, player=""):
|
def async_query(self, *command, player=""):
|
||||||
"""Abstract out the JSON-RPC connection."""
|
"""Abstract out the JSON-RPC connection."""
|
||||||
response = None
|
|
||||||
auth = None if self._username is None else aiohttp.BasicAuth(
|
auth = None if self._username is None else aiohttp.BasicAuth(
|
||||||
self._username, self._password)
|
self._username, self._password)
|
||||||
url = "http://{}:{}/jsonrpc.js".format(
|
url = "http://{}:{}/jsonrpc.js".format(
|
||||||
|
@ -138,22 +137,17 @@ class LogitechMediaServer(object):
|
||||||
data=data,
|
data=data,
|
||||||
auth=auth)
|
auth=auth)
|
||||||
|
|
||||||
if response.status == 200:
|
if response.status != 200:
|
||||||
data = yield from response.json()
|
|
||||||
else:
|
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Query failed, response code: %s Full message: %s",
|
"Query failed, response code: %s Full message: %s",
|
||||||
response.status, response)
|
response.status, response)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except (asyncio.TimeoutError,
|
data = yield from response.json()
|
||||||
aiohttp.errors.ClientError,
|
|
||||||
aiohttp.errors.ClientDisconnectedError) as error:
|
except (asyncio.TimeoutError, aiohttp.ClientError) as error:
|
||||||
_LOGGER.error("Failed communicating with LMS: %s", type(error))
|
_LOGGER.error("Failed communicating with LMS: %s", type(error))
|
||||||
return False
|
return False
|
||||||
finally:
|
|
||||||
if response is not None:
|
|
||||||
yield from response.release()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return data['result']
|
return data['result']
|
||||||
|
|
|
@ -425,12 +425,12 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
||||||
return self._async_call_service(
|
return self._async_call_service(
|
||||||
SERVICE_VOLUME_MUTE, data, allow_override=True)
|
SERVICE_VOLUME_MUTE, data, allow_override=True)
|
||||||
|
|
||||||
def async_set_volume_level(self, volume_level):
|
def async_set_volume_level(self, volume):
|
||||||
"""Set volume level, range 0..1.
|
"""Set volume level, range 0..1.
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level}
|
data = {ATTR_MEDIA_VOLUME_LEVEL: volume}
|
||||||
return self._async_call_service(
|
return self._async_call_service(
|
||||||
SERVICE_VOLUME_SET, data, allow_override=True)
|
SERVICE_VOLUME_SET, data, allow_override=True)
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue