commit
cd8723f742
11
.coveragerc
11
.coveragerc
|
@ -147,11 +147,12 @@ omit =
|
|||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/alarm_control_panel/totalconnect.py
|
||||
homeassistant/components/apiai.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/concord232.py
|
||||
|
@ -225,6 +226,7 @@ omit =
|
|||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/lifx_legacy.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/tikteck.py
|
||||
|
@ -235,6 +237,7 @@ omit =
|
|||
homeassistant/components/light/zengge.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/lock/nuki.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/media_player/anthemav.py
|
||||
homeassistant/components/media_player/apple_tv.py
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
|
@ -323,6 +326,7 @@ omit =
|
|||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/comed_hourly_pricing.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/crimereports.py
|
||||
homeassistant/components/sensor/cups.py
|
||||
homeassistant/components/sensor/currencylayer.py
|
||||
homeassistant/components/sensor/darksky.py
|
||||
|
@ -332,6 +336,7 @@ omit =
|
|||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
homeassistant/components/sensor/eddystone_temperature.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/emoncms.py
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
|
@ -357,9 +362,11 @@ omit =
|
|||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/lyft.py
|
||||
homeassistant/components/sensor/metoffice.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/modem_callerid.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
homeassistant/components/sensor/mvglive.py
|
||||
homeassistant/components/sensor/netdata.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nut.py
|
||||
|
@ -431,12 +438,12 @@ omit =
|
|||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/upnp.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/metoffice.py
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/weather/zamg.py
|
||||
homeassistant/components/zeroconf.py
|
||||
homeassistant/components/zwave/__init__.py
|
||||
homeassistant/components/zwave/util.py
|
||||
homeassistant/components/zwave/workaround.py
|
||||
|
||||
|
||||
[report]
|
||||
|
|
|
@ -1,15 +1,4 @@
|
|||
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/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 is a home automation platform running on Python 3. The
|
||||
goal of Home Assistant is to be able to track and control all devices at
|
||||
home and offer a platform for automating control.
|
||||
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.
|
||||
|
||||
To get started:
|
||||
|
||||
|
@ -12,83 +10,22 @@ To get started:
|
|||
python3 -m pip install homeassistant
|
||||
hass --open-ui
|
||||
|
||||
Check out `the website <https://home-assistant.io>`__ for `a
|
||||
demo <https://home-assistant.io/demo/>`__, installation instructions,
|
||||
tutorials and documentation.
|
||||
Check out `home-assistant.io <https://home-assistant.io>`__ for `a
|
||||
demo <https://home-assistant.io/demo/>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
|
||||
`tutorials <https://home-assistant.io/getting-started/automation-2/>`__ and `documentation <https://home-assistant.io/docs/>`__.
|
||||
|
||||
|screenshot-states|
|
||||
|
||||
Examples of devices Home Assistant can interface with:
|
||||
Featured integrations
|
||||
---------------------
|
||||
|
||||
- Monitoring connected devices to a wireless router:
|
||||
`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/>`__
|
||||
|screenshot-components|
|
||||
|
||||
Build home automation on top of your devices:
|
||||
|
||||
- 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
|
||||
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/>`__.
|
||||
|
||||
If you run into issues while using Home Assistant or during development
|
||||
of a component, check the `Home Assistant help
|
||||
section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
||||
of a component, check the `Home Assistant help 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
|
||||
: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
|
||||
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
|
||||
: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
|
||||
|
||||
|
||||
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():
|
||||
"""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
|
||||
)
|
||||
|
||||
hass.start()
|
||||
return hass.exit_code
|
||||
return hass.start()
|
||||
|
||||
|
||||
def try_to_restart() -> None:
|
||||
|
@ -359,11 +369,13 @@ def try_to_restart() -> None:
|
|||
|
||||
def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
validate_python()
|
||||
|
||||
attempt_use_uvloop()
|
||||
|
||||
if sys.version_info[:3] < (3, 5, 3):
|
||||
monkey_patch_asyncio()
|
||||
|
||||
validate_python()
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
if args.script is not None:
|
||||
|
|
|
@ -74,8 +74,6 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||
This method is a coroutine.
|
||||
"""
|
||||
start = time()
|
||||
hass.async_track_tasks()
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
|
@ -140,10 +138,10 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||
continue
|
||||
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()
|
||||
_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)
|
||||
return hass
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"""
|
||||
|
||||
Interfaces with Alarm.com alarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import asyncio
|
||||
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 (
|
||||
|
@ -15,10 +15,9 @@ from homeassistant.const import (
|
|||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN, CONF_CODE,
|
||||
CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
REQUIREMENTS = ['https://github.com/Xorso/pyalarmdotcom'
|
||||
'/archive/0.1.1.zip'
|
||||
'#pyalarmdotcom==0.1.1']
|
||||
REQUIREMENTS = ['pyalarmdotcom==0.2.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -32,14 +31,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup an Alarm.com control panel."""
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a Alarm.com control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
username = config.get(CONF_USERNAME)
|
||||
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):
|
||||
|
@ -47,18 +49,30 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||
|
||||
def __init__(self, hass, name, code, username, password):
|
||||
"""Initialize the Alarm.com status."""
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
self._alarm = Alarmdotcom(username, password, timeout=10)
|
||||
from pyalarmdotcom import Alarmdotcom
|
||||
_LOGGER.debug('Setting up Alarm.com...')
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._websession = async_get_clientsession(self._hass)
|
||||
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."""
|
||||
self._state = self._alarm.state
|
||||
yield from self._alarm.async_update()
|
||||
return self._alarm.state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -73,45 +87,36 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._state == 'Disarmed':
|
||||
if self._alarm.state.lower() == 'disarmed':
|
||||
return STATE_ALARM_DISARMED
|
||||
elif self._state == 'Armed Stay':
|
||||
elif self._alarm.state.lower() == 'armed stay':
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._state == 'Armed Away':
|
||||
elif self._alarm.state.lower() == 'armed away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, 'disarming home'):
|
||||
return
|
||||
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()
|
||||
if self._validate_code(code):
|
||||
yield from self._alarm.async_alarm_disarm()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
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_stay()
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm hom command."""
|
||||
if self._validate_code(code):
|
||||
yield from self._alarm.async_alarm_arm_home()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
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()
|
||||
if self._validate_code(code):
|
||||
yield from self._alarm.async_alarm_arm_away()
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
def _validate_code(self, code):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning('Wrong code entered for %s', state)
|
||||
_LOGGER.warning('Wrong code entered.')
|
||||
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 (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
|
||||
|
||||
DOMAIN = 'android_ip_webcam'
|
||||
REQUIREMENTS = ["pydroid-ipcam==0.6"]
|
||||
REQUIREMENTS = ['pydroid-ipcam==0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
DATA_IP_WEBCAM = 'android_ip_webcam'
|
||||
|
||||
ATTR_AUD_CONNS = 'Audio Connections'
|
||||
ATTR_HOST = 'host'
|
||||
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 = {
|
||||
'audio_connections': 'Audio Connections',
|
||||
|
@ -123,15 +130,6 @@ SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
|
|||
'battery_voltage', 'light', 'motion', 'pressure', 'proximity',
|
||||
'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({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
|
@ -153,7 +151,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the IP Webcam component."""
|
||||
"""Set up the IP Webcam component."""
|
||||
from pydroid_ipcam import PyDroidIPCam
|
||||
|
||||
webcams = hass.data[DATA_IP_WEBCAM] = {}
|
||||
|
@ -161,7 +159,7 @@ def async_setup(hass, config):
|
|||
|
||||
@asyncio.coroutine
|
||||
def async_setup_ipcamera(cam_config):
|
||||
"""Setup a ip camera."""
|
||||
"""Set up an IP camera."""
|
||||
host = cam_config[CONF_HOST]
|
||||
username = cam_config.get(CONF_USERNAME)
|
||||
password = cam_config.get(CONF_PASSWORD)
|
||||
|
@ -171,7 +169,7 @@ def async_setup(hass, config):
|
|||
sensors = cam_config[CONF_SENSORS]
|
||||
motion = cam_config[CONF_MOTION_SENSOR]
|
||||
|
||||
# init ip webcam
|
||||
# Init ip webcam
|
||||
cam = PyDroidIPCam(
|
||||
hass.loop, websession, host, cam_config[CONF_PORT],
|
||||
username=username, password=password,
|
||||
|
@ -192,7 +190,7 @@ def async_setup(hass, config):
|
|||
|
||||
@asyncio.coroutine
|
||||
def async_update_data(now):
|
||||
"""Update data from ipcam in SCAN_INTERVAL."""
|
||||
"""Update data from IP camera in SCAN_INTERVAL."""
|
||||
yield from cam.update()
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
|
||||
|
||||
|
@ -201,7 +199,7 @@ def async_setup(hass, config):
|
|||
|
||||
yield from async_update_data(None)
|
||||
|
||||
# load platforms
|
||||
# Load platforms
|
||||
webcams[host] = cam
|
||||
|
||||
mjpeg_camera = {
|
||||
|
|
|
@ -50,9 +50,11 @@ def setup(hass, config):
|
|||
hass.http.register_view(APIDomainServicesView)
|
||||
hass.http.register_view(APIEventForwardingView)
|
||||
hass.http.register_view(APIComponentsView)
|
||||
hass.http.register_view(APIErrorLogView)
|
||||
hass.http.register_view(APITemplateView)
|
||||
|
||||
hass.http.register_static_path(
|
||||
URL_API_ERROR_LOG, hass.config.path(ERROR_LOG_FILENAME), False)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -402,20 +404,6 @@ class APIComponentsView(HomeAssistantView):
|
|||
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):
|
||||
"""View to handle requests."""
|
||||
|
||||
|
|
|
@ -12,10 +12,11 @@ import os
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.core import CoreState
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import (
|
||||
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.exceptions import HomeAssistantError
|
||||
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({
|
||||
CONF_ALIAS: cv.string,
|
||||
vol.Optional(CONF_INITIAL_STATE,
|
||||
default=DEFAULT_INITIAL_STATE): cv.boolean,
|
||||
vol.Optional(CONF_INITIAL_STATE): cv.boolean,
|
||||
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
|
||||
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
|
||||
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
|
||||
|
@ -101,15 +101,13 @@ TRIGGER_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.
|
||||
|
||||
Check all automation if no entity_id specified.
|
||||
Async friendly.
|
||||
"""
|
||||
entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN)
|
||||
return any(hass.states.is_state(entity_id, STATE_ON)
|
||||
for entity_id in entity_ids)
|
||||
return hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
|
||||
def turn_on(hass, entity_id=None):
|
||||
|
@ -231,7 +229,6 @@ class AutomationEntity(ToggleEntity):
|
|||
self._async_detach_triggers = None
|
||||
self._cond_func = cond_func
|
||||
self._async_action = async_action
|
||||
self._enabled = False
|
||||
self._last_triggered = None
|
||||
self._hidden = hidden
|
||||
self._initial_state = initial_state
|
||||
|
@ -261,38 +258,54 @@ class AutomationEntity(ToggleEntity):
|
|||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
return self._enabled
|
||||
return self._async_detach_triggers is not None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
"""Startup with initial state or previous state."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state is None:
|
||||
if self._initial_state:
|
||||
yield from self.async_enable()
|
||||
enable_automation = DEFAULT_INITIAL_STATE
|
||||
|
||||
if self._initial_state is not None:
|
||||
enable_automation = self._initial_state
|
||||
else:
|
||||
self._last_triggered = state.attributes.get('last_triggered')
|
||||
if state.state == STATE_ON:
|
||||
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')
|
||||
|
||||
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()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn the entity on and update the state."""
|
||||
if self._enabled:
|
||||
if self.is_on:
|
||||
return
|
||||
|
||||
yield from self.async_enable()
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the entity off."""
|
||||
if not self._enabled:
|
||||
if not self.is_on:
|
||||
return
|
||||
|
||||
self._async_detach_triggers()
|
||||
self._async_detach_triggers = None
|
||||
self._enabled = False
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -318,12 +331,12 @@ class AutomationEntity(ToggleEntity):
|
|||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if self._enabled:
|
||||
if self.is_on:
|
||||
return
|
||||
|
||||
self._async_detach_triggers = yield from self._async_attach_triggers(
|
||||
self.async_trigger)
|
||||
self._enabled = True
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -342,7 +355,7 @@ def _async_process_config(hass, config, component):
|
|||
list_no)
|
||||
|
||||
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, {}),
|
||||
name)
|
||||
|
|
|
@ -2,15 +2,15 @@
|
|||
Offer event listening automation rules.
|
||||
|
||||
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 logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.core import callback, CoreState
|
||||
from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
CONF_EVENT_TYPE = "event_type"
|
||||
|
@ -31,6 +31,19 @@ def async_trigger(hass, config, action):
|
|||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
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
|
||||
def handle_event(event):
|
||||
"""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
|
||||
pressed_time = dt_util.utcnow()
|
||||
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:
|
||||
cancel_pressed_more_than = track_point_in_utc_time(
|
||||
hass,
|
||||
|
@ -88,7 +88,7 @@ def async_trigger(hass, config, action):
|
|||
held_time = dt_util.utcnow() - pressed_time
|
||||
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:
|
||||
call_action()
|
||||
hass.add_job(call_action)
|
||||
|
||||
hass.data['litejet_system'].on_switch_pressed(number, pressed)
|
||||
hass.data['litejet_system'].on_switch_released(number, released)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Offer MQTT listening automation rules.
|
||||
|
||||
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 json
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Offer numeric state listening automation rules.
|
||||
|
||||
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 logging
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Offer state listening automation rules.
|
||||
|
||||
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 voluptuous as vol
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Offer sun based automation rules.
|
||||
|
||||
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
|
||||
from datetime import timedelta
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Offer template automation rules.
|
||||
|
||||
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 logging
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Offer time listening automation rules.
|
||||
|
||||
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 logging
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Offer zone automation rules.
|
||||
|
||||
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 voluptuous as vol
|
||||
|
|
|
@ -11,7 +11,7 @@ DEPENDENCIES = ['blink']
|
|||
|
||||
|
||||
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:
|
||||
return
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.const import (
|
|||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.1.0']
|
||||
REQUIREMENTS = ['pyhik==0.1.2']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
|
@ -33,7 +33,6 @@ ATTR_DELAY = 'delay'
|
|||
DEVICE_CLASS_MAP = {
|
||||
'Motion': 'motion',
|
||||
'Line Crossing': 'motion',
|
||||
'IO Trigger': None,
|
||||
'Field Detection': 'motion',
|
||||
'Video Loss': None,
|
||||
'Tamper Detection': 'motion',
|
||||
|
@ -47,6 +46,7 @@ DEVICE_CLASS_MAP = {
|
|||
'Bad Video': None,
|
||||
'PIR Alarm': 'motion',
|
||||
'Face Detection': 'motion',
|
||||
'Scene Change Detection': 'motion',
|
||||
}
|
||||
|
||||
CUSTOMIZE_SCHEMA = vol.Schema({
|
||||
|
@ -91,24 +91,30 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
|
||||
entities = []
|
||||
|
||||
for sensor in data.sensors:
|
||||
# Build sensor name, then parse customize config.
|
||||
sensor_name = sensor.replace(' ', '_')
|
||||
for sensor, channel_list in data.sensors.items():
|
||||
for channel in channel_list:
|
||||
# Build sensor name, then parse customize config.
|
||||
if data.type == 'NVR':
|
||||
sensor_name = '{}_{}'.format(
|
||||
sensor.replace(' ', '_'), channel[1])
|
||||
else:
|
||||
sensor_name = sensor.replace(' ', '_')
|
||||
|
||||
custom = customize.get(sensor_name.lower(), {})
|
||||
ignore = custom.get(CONF_IGNORED)
|
||||
delay = custom.get(CONF_DELAY)
|
||||
custom = customize.get(sensor_name.lower(), {})
|
||||
ignore = custom.get(CONF_IGNORED)
|
||||
delay = custom.get(CONF_DELAY)
|
||||
|
||||
_LOGGER.debug('Entity: %s - %s, Options - Ignore: %s, Delay: %s',
|
||||
data.name, sensor_name, ignore, delay)
|
||||
if not ignore:
|
||||
entities.append(HikvisionBinarySensor(hass, sensor, data, delay))
|
||||
_LOGGER.debug('Entity: %s - %s, Options - Ignore: %s, Delay: %s',
|
||||
data.name, sensor_name, ignore, delay)
|
||||
if not ignore:
|
||||
entities.append(HikvisionBinarySensor(
|
||||
hass, sensor, channel[1], data, delay))
|
||||
|
||||
add_entities(entities)
|
||||
|
||||
|
||||
class HikvisionData(object):
|
||||
"""Hikvision camera event stream object."""
|
||||
"""Hikvision device event stream object."""
|
||||
|
||||
def __init__(self, hass, url, port, name, username, password):
|
||||
"""Initialize the data oject."""
|
||||
|
@ -144,25 +150,40 @@ class HikvisionData(object):
|
|||
|
||||
@property
|
||||
def cam_id(self):
|
||||
"""Return camera id."""
|
||||
"""Return device id."""
|
||||
return self.camdata.get_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return camera name."""
|
||||
"""Return device 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):
|
||||
"""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."""
|
||||
self._hass = hass
|
||||
self._cam = cam
|
||||
self._name = self._cam.name + ' ' + sensor
|
||||
self._id = self._cam.cam_id + '.' + 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:
|
||||
self._delay = 0
|
||||
|
@ -176,11 +197,11 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
|||
|
||||
def _sensor_state(self):
|
||||
"""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):
|
||||
"""Extract sensor last update time."""
|
||||
return self._cam.sensors[self._sensor][3]
|
||||
return self._cam.get_attributes(self._sensor, self._channel)[3]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
|
@ -32,7 +32,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
InsteonPLMBinarySensorDevice(hass, plm, address, name)
|
||||
)
|
||||
|
||||
hass.async_add_job(async_add_devices(device_list))
|
||||
async_add_devices(device_list)
|
||||
|
||||
|
||||
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 logging
|
||||
import datetime
|
||||
|
@ -6,8 +11,8 @@ import datetime
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN,
|
||||
CONF_NAME, WEEKDAYS)
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, STATE_UNKNOWN, CONF_NAME, WEEKDAYS)
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
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):
|
||||
"""Setup the Workday sensor."""
|
||||
"""Set up the Workday sensor."""
|
||||
import holidays
|
||||
|
||||
# Get the Sensor name from the config
|
||||
sensor_name = config.get(CONF_NAME)
|
||||
|
||||
# Get the country code from the config
|
||||
country = config.get(CONF_COUNTRY)
|
||||
|
||||
# Get the province from the config
|
||||
province = config.get(CONF_PROVINCE)
|
||||
|
||||
# Get the list of workdays from the config
|
||||
workdays = config.get(CONF_WORKDAYS)
|
||||
|
||||
# Get the list of excludes from the config
|
||||
excludes = config.get(CONF_EXCLUDES)
|
||||
|
||||
# Instantiate the holidays module for the current year
|
||||
year = datetime.datetime.now().year
|
||||
obj_holidays = getattr(holidays, country)(years=year)
|
||||
|
||||
# Also apply the provience, if available for the configured country
|
||||
if province:
|
||||
if province not in obj_holidays.PROVINCES:
|
||||
_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,
|
||||
years=year)
|
||||
|
||||
# Output found public holidays via the debug channel
|
||||
_LOGGER.debug("Found the following holidays for your configuration:")
|
||||
for date, name in sorted(obj_holidays.items()):
|
||||
_LOGGER.debug("%s %s", date, name)
|
||||
|
||||
# Add ourselves as device
|
||||
add_devices([IsWorkdaySensor(obj_holidays, workdays,
|
||||
excludes, sensor_name)], True)
|
||||
add_devices([IsWorkdaySensor(
|
||||
obj_holidays, workdays, excludes, sensor_name)], True)
|
||||
|
||||
|
||||
def day_to_string(day):
|
||||
|
@ -122,7 +114,6 @@ class IsWorkdaySensor(Entity):
|
|||
|
||||
def is_include(self, day, now):
|
||||
"""Check if given day is in the includes list."""
|
||||
# Check includes
|
||||
if day in self._workdays:
|
||||
return True
|
||||
elif 'holiday' in self._workdays and now in self._obj_holidays:
|
||||
|
@ -132,7 +123,6 @@ class IsWorkdaySensor(Entity):
|
|||
|
||||
def is_exclude(self, day, now):
|
||||
"""Check if given day is in the excludes list."""
|
||||
# Check excludes
|
||||
if day in self._excludes:
|
||||
return True
|
||||
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/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (CONF_USERNAME,
|
||||
CONF_PASSWORD,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_ARMED)
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, ATTR_FRIENDLY_NAME, ATTR_ARMED)
|
||||
from homeassistant.helpers import discovery
|
||||
|
||||
REQUIREMENTS = ['blinkpy==0.5.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'blink'
|
||||
REQUIREMENTS = ['blinkpy==0.5.2']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
|
@ -50,7 +52,7 @@ class BlinkSystem(object):
|
|||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup Blink System."""
|
||||
"""Set up Blink System."""
|
||||
hass.data[DOMAIN] = BlinkSystem(config)
|
||||
discovery.load_platform(hass, 'camera', 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.refresh()
|
||||
|
||||
hass.services.register(DOMAIN, 'snap_picture', snap_picture,
|
||||
schema=SNAP_PICTURE_SCHEMA)
|
||||
hass.services.register(DOMAIN, 'arm_camera', arm_camera,
|
||||
schema=ARM_CAMERA_SCHEMA)
|
||||
hass.services.register(DOMAIN, 'arm_system', arm_system,
|
||||
schema=ARM_SYSTEM_SCHEMA)
|
||||
hass.services.register(
|
||||
DOMAIN, 'snap_picture', snap_picture, schema=SNAP_PICTURE_SCHEMA)
|
||||
hass.services.register(
|
||||
DOMAIN, 'arm_camera', arm_camera, schema=ARM_CAMERA_SCHEMA)
|
||||
hass.services.register(
|
||||
DOMAIN, 'arm_system', arm_system, schema=ARM_SYSTEM_SCHEMA)
|
||||
|
||||
return True
|
||||
|
|
|
@ -7,6 +7,7 @@ https://home-assistant.io/components/camera/
|
|||
"""
|
||||
import asyncio
|
||||
import collections
|
||||
from contextlib import suppress
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import hashlib
|
||||
|
@ -58,7 +59,6 @@ def async_get_image(hass, entity_id, timeout=10):
|
|||
state.attributes.get(ATTR_ENTITY_PICTURE)
|
||||
)
|
||||
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
response = yield from websession.get(url)
|
||||
|
@ -70,13 +70,9 @@ def async_get_image(hass, entity_id, timeout=10):
|
|||
image = yield from response.read()
|
||||
return image
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
raise HomeAssistantError("Can't connect to {0}".format(url))
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
|
@ -172,7 +168,7 @@ class Camera(Entity):
|
|||
if not img_bytes:
|
||||
break
|
||||
|
||||
if img_bytes is not None and img_bytes != last_image:
|
||||
if img_bytes and img_bytes != last_image:
|
||||
write(img_bytes)
|
||||
|
||||
# Chrome seems to always ignore first picture,
|
||||
|
@ -185,8 +181,8 @@ class Camera(Entity):
|
|||
|
||||
yield from asyncio.sleep(.5)
|
||||
|
||||
except (asyncio.CancelledError, ConnectionResetError):
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Stream closed by frontend.")
|
||||
response = None
|
||||
|
||||
finally:
|
||||
|
@ -268,16 +264,14 @@ class CameraImageView(CameraView):
|
|||
@asyncio.coroutine
|
||||
def handle(self, request, camera):
|
||||
"""Serve camera image."""
|
||||
try:
|
||||
image = yield from camera.async_camera_image()
|
||||
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
||||
with async_timeout.timeout(10, loop=request.app['hass'].loop):
|
||||
image = yield from camera.async_camera_image()
|
||||
|
||||
if image is None:
|
||||
return web.Response(status=500)
|
||||
if image:
|
||||
return web.Response(body=image)
|
||||
|
||||
return web.Response(body=image)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
return web.Response(status=500)
|
||||
|
||||
|
||||
class CameraMjpegStream(CameraView):
|
||||
|
|
|
@ -16,9 +16,9 @@ from homeassistant.const import (
|
|||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
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__)
|
||||
|
||||
|
@ -125,7 +125,7 @@ class AmcrestCam(Camera):
|
|||
stream_coro = websession.get(
|
||||
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
|
||||
def name(self):
|
||||
|
|
|
@ -8,14 +8,14 @@ import asyncio
|
|||
import logging
|
||||
|
||||
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.ffmpeg import (
|
||||
DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
|
||||
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']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -69,26 +69,10 @@ class FFmpegCamera(Camera):
|
|||
yield from stream.open_camera(
|
||||
self._input, extra_cmd=self._extra_arguments)
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = '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()
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
|
@ -66,9 +66,13 @@ class FoscamCamera(Camera):
|
|||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
# Send the request to snap a picture and return raw jpg data
|
||||
response = requests.get(self._snap_picture_url, timeout=10)
|
||||
|
||||
return response.content
|
||||
# Handle exception if host is not reachable or url failed
|
||||
try:
|
||||
response = requests.get(self._snap_picture_url, timeout=10)
|
||||
except requests.exceptions.ConnectionError:
|
||||
return None
|
||||
else:
|
||||
return response.content
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
|
@ -107,7 +107,6 @@ class GenericCamera(Camera):
|
|||
None, fetch)
|
||||
# async
|
||||
else:
|
||||
response = None
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
|
@ -117,14 +116,9 @@ class GenericCamera(Camera):
|
|||
except asyncio.TimeoutError:
|
||||
_LOGGER.error('Timeout getting camera image')
|
||||
return self._last_image
|
||||
except (aiohttp.errors.ClientError,
|
||||
aiohttp.errors.DisconnectedError,
|
||||
aiohttp.errors.HttpProcessingError) as err:
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error('Error getting new camera image: %s', err)
|
||||
return self._last_image
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
self._last_url = url
|
||||
return self._last_image
|
||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
|||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -93,7 +93,6 @@ class MjpegCamera(Camera):
|
|||
return image
|
||||
|
||||
websession = async_get_clientsession(self.hass)
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = yield from websession.get(
|
||||
|
@ -105,14 +104,9 @@ class MjpegCamera(Camera):
|
|||
except asyncio.TimeoutError:
|
||||
_LOGGER.error('Timeout getting camera image')
|
||||
|
||||
except (aiohttp.errors.ClientError,
|
||||
aiohttp.errors.ClientDisconnectedError) as err:
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error('Error getting new camera image: %s', err)
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
if self._username and self._password:
|
||||
|
@ -140,7 +134,7 @@ class MjpegCamera(Camera):
|
|||
websession = async_get_clientsession(self.hass)
|
||||
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
|
||||
def name(self):
|
||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.components.camera import (
|
|||
Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_create_clientsession,
|
||||
async_aiohttp_proxy_stream)
|
||||
async_aiohttp_proxy_web)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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',
|
||||
'query': 'SYNO.'
|
||||
}
|
||||
query_req = None
|
||||
try:
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
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']
|
||||
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)
|
||||
return False
|
||||
|
||||
finally:
|
||||
if query_req is not None:
|
||||
yield from query_req.release()
|
||||
|
||||
# Authticate to NAS to get a session id
|
||||
syno_auth_url = SYNO_API_URL.format(
|
||||
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,
|
||||
params=camera_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_camera_url)
|
||||
return False
|
||||
|
||||
camera_resp = yield from camera_req.json()
|
||||
cameras = camera_resp['data']['cameras']
|
||||
yield from camera_req.release()
|
||||
|
||||
# add cameras
|
||||
devices = []
|
||||
|
@ -172,7 +166,6 @@ def get_session_id(hass, websession, username, password, login_url, timeout):
|
|||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'
|
||||
}
|
||||
auth_req = None
|
||||
try:
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
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()
|
||||
return auth_resp['data']['sid']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", login_url)
|
||||
return False
|
||||
|
||||
finally:
|
||||
if auth_req is not None:
|
||||
yield from auth_req.release()
|
||||
|
||||
|
||||
class SynologyCamera(Camera):
|
||||
"""An implementation of a Synology NAS based IP camera."""
|
||||
|
@ -235,12 +224,11 @@ class SynologyCamera(Camera):
|
|||
image_url,
|
||||
params=image_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", image_url)
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Error fetching %s", image_url)
|
||||
return None
|
||||
|
||||
image = yield from response.read()
|
||||
yield from response.release()
|
||||
|
||||
return image
|
||||
|
||||
|
@ -260,7 +248,7 @@ class SynologyCamera(Camera):
|
|||
stream_coro = self._websession.get(
|
||||
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
|
||||
def name(self):
|
||||
|
|
|
@ -692,18 +692,16 @@ class ClimateDevice(Entity):
|
|||
|
||||
def _convert_for_display(self, temp):
|
||||
"""Convert temperature into preferred units for display purposes."""
|
||||
if (temp is None or not isinstance(temp, Number) or
|
||||
self.temperature_unit == self.unit_of_measurement):
|
||||
if temp is None or not isinstance(temp, Number):
|
||||
return temp
|
||||
|
||||
value = convert_temperature(temp, self.temperature_unit,
|
||||
self.unit_of_measurement)
|
||||
|
||||
if self.temperature_unit != self.unit_of_measurement:
|
||||
temp = convert_temperature(temp, self.temperature_unit,
|
||||
self.unit_of_measurement)
|
||||
# Round in the units appropriate
|
||||
if self.precision == PRECISION_HALVES:
|
||||
return round(value * 2) / 2.0
|
||||
return round(temp * 2) / 2.0
|
||||
elif self.precision == PRECISION_TENTHS:
|
||||
return round(value, 1)
|
||||
return round(temp, 1)
|
||||
else:
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(value)
|
||||
return round(temp)
|
||||
|
|
|
@ -16,11 +16,15 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
STATE_MANUAL = "manual"
|
||||
STATE_BOOST = "boost"
|
||||
STATE_COMFORT = "comfort"
|
||||
STATE_LOWERING = "lowering"
|
||||
|
||||
HM_STATE_MAP = {
|
||||
"AUTO_MODE": STATE_AUTO,
|
||||
"MANU_MODE": STATE_MANUAL,
|
||||
"BOOST_MODE": STATE_BOOST,
|
||||
"COMFORT_MODE": STATE_COMFORT,
|
||||
"LOWERING_MODE": STATE_LOWERING
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice)
|
||||
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"}
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
from homeassistant.components.tado import DATA_TADO
|
||||
|
||||
_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):
|
||||
"""Setup the climate platform."""
|
||||
# get the PyTado object from the hub component
|
||||
"""Set up the Tado climate platform."""
|
||||
tado = hass.data[DATA_TADO]
|
||||
|
||||
try:
|
||||
|
@ -45,10 +45,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
climate_devices = []
|
||||
for zone in zones:
|
||||
climate_devices.append(create_climate_device(tado, hass,
|
||||
zone,
|
||||
zone['name'],
|
||||
zone['id']))
|
||||
climate_devices.append(create_climate_device(
|
||||
tado, hass, zone, zone['name'], zone['id']))
|
||||
|
||||
if len(climate_devices) > 0:
|
||||
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):
|
||||
"""Create a climate device."""
|
||||
"""Create a Tado climate device."""
|
||||
capabilities = tado.get_capabilities(zone_id)
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
min_temp = float(capabilities["temperatures"]["celsius"]["min"])
|
||||
max_temp = float(capabilities["temperatures"]["celsius"]["max"])
|
||||
ac_mode = capabilities["type"] != "HEATING"
|
||||
min_temp = float(capabilities['temperatures']['celsius']['min'])
|
||||
max_temp = float(capabilities['temperatures']['celsius']['max'])
|
||||
ac_mode = capabilities['type'] != 'HEATING'
|
||||
|
||||
data_id = 'zone {} {}'.format(name, zone_id)
|
||||
device = TadoClimate(tado,
|
||||
|
@ -74,10 +72,10 @@ def create_climate_device(tado, hass, zone, name, zone_id):
|
|||
ac_mode)
|
||||
|
||||
tado.add_sensor(data_id, {
|
||||
"id": zone_id,
|
||||
"zone": zone,
|
||||
"name": name,
|
||||
"climate": device
|
||||
'id': zone_id,
|
||||
'zone': zone,
|
||||
'name': name,
|
||||
'climate': device
|
||||
})
|
||||
|
||||
return device
|
||||
|
@ -89,7 +87,7 @@ class TadoClimate(ClimateDevice):
|
|||
def __init__(self, store, zone_name, zone_id, data_id,
|
||||
min_temp, max_temp, ac_mode,
|
||||
tolerance=0.3):
|
||||
"""Initialization of TadoClimate device."""
|
||||
"""Initialization of Tado climate device."""
|
||||
self._store = store
|
||||
self._data_id = data_id
|
||||
|
||||
|
@ -202,8 +200,7 @@ class TadoClimate(ClimateDevice):
|
|||
data = self._store.get_data(self._data_id)
|
||||
|
||||
if data is None:
|
||||
_LOGGER.debug('Recieved no data for zone %s',
|
||||
self.zone_name)
|
||||
_LOGGER.debug("Recieved no data for zone %s", self.zone_name)
|
||||
return
|
||||
|
||||
if 'sensorDataPoints' in data:
|
||||
|
@ -232,11 +229,11 @@ class TadoClimate(ClimateDevice):
|
|||
|
||||
if 'tadoMode' in data:
|
||||
mode = data['tadoMode']
|
||||
self._is_away = mode == "AWAY"
|
||||
self._is_away = mode == 'AWAY'
|
||||
|
||||
if 'setting' in data:
|
||||
power = data['setting']['power']
|
||||
if power == "OFF":
|
||||
if power == 'OFF':
|
||||
self._current_operation = CONST_MODE_OFF
|
||||
self._device_is_active = False
|
||||
else:
|
||||
|
@ -249,48 +246,47 @@ class TadoClimate(ClimateDevice):
|
|||
overlay = False
|
||||
termination = ""
|
||||
|
||||
# if you set mode manualy to off, there will be an overlay
|
||||
# and a termination, but we want to see the mode "OFF"
|
||||
# If you set mode manualy to off, there will be an overlay
|
||||
# and a termination, but we want to see the mode "OFF"
|
||||
|
||||
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._current_operation = termination
|
||||
else:
|
||||
# there is no overlay, the mode will always be
|
||||
# "SMART_SCHEDULE"
|
||||
# There is no overlay, the mode will always be
|
||||
# "SMART_SCHEDULE"
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
def _control_heating(self):
|
||||
"""Send new target temperature to mytado."""
|
||||
if not self._active and None not in (self._cur_temp,
|
||||
self._target_temp):
|
||||
if not self._active and None not in (
|
||||
self._cur_temp, self._target_temp):
|
||||
self._active = True
|
||||
_LOGGER.info('Obtained current and target temperature. '
|
||||
'tado thermostat active.')
|
||||
_LOGGER.info("Obtained current and target temperature. "
|
||||
"Tado thermostat active")
|
||||
|
||||
if not self._active or self._current_operation == self._overlay_mode:
|
||||
return
|
||||
|
||||
if self._current_operation == CONST_MODE_SMART_SCHEDULE:
|
||||
_LOGGER.info('Switching mytado.com to SCHEDULE (default) '
|
||||
'for zone %s', self.zone_name)
|
||||
_LOGGER.info("Switching mytado.com to SCHEDULE (default) "
|
||||
"for zone %s", self.zone_name)
|
||||
self._store.reset_zone_overlay(self.zone_id)
|
||||
self._overlay_mode = self._current_operation
|
||||
return
|
||||
|
||||
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._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL)
|
||||
self._overlay_mode = self._current_operation
|
||||
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._store.set_zone_overlay(self.zone_id,
|
||||
self._current_operation,
|
||||
self._target_temp)
|
||||
self._store.set_zone_overlay(
|
||||
self.zone_id, self._current_operation, self._target_temp)
|
||||
|
||||
self._overlay_mode = self._current_operation
|
||||
|
|
|
@ -19,7 +19,6 @@ DEPENDENCIES = ['wink']
|
|||
STATE_AUX = 'aux'
|
||||
STATE_ECO = 'eco'
|
||||
STATE_FAN = 'fan'
|
||||
SPEED_LOWEST = 'lowest'
|
||||
SPEED_LOW = 'low'
|
||||
SPEED_MEDIUM = 'medium'
|
||||
SPEED_HIGH = 'high'
|
||||
|
@ -400,7 +399,7 @@ class WinkAC(WinkDevice, ClimateDevice):
|
|||
op_list.append(STATE_COOL)
|
||||
if 'auto_eco' in modes:
|
||||
op_list.append(STATE_ECO)
|
||||
if 'fan_eco' in modes:
|
||||
if 'fan_only' in modes:
|
||||
op_list.append(STATE_FAN)
|
||||
return op_list
|
||||
|
||||
|
@ -439,9 +438,7 @@ class WinkAC(WinkDevice, ClimateDevice):
|
|||
def current_fan_mode(self):
|
||||
"""Return the current fan mode."""
|
||||
speed = self.wink.current_fan_speed()
|
||||
if speed <= 0.3 and speed >= 0.0:
|
||||
return SPEED_LOWEST
|
||||
elif speed <= 0.5 and speed > 0.3:
|
||||
if speed <= 0.4 and speed > 0.3:
|
||||
return SPEED_LOW
|
||||
elif speed <= 0.8 and speed > 0.5:
|
||||
return SPEED_MEDIUM
|
||||
|
@ -453,14 +450,12 @@ class WinkAC(WinkDevice, ClimateDevice):
|
|||
@property
|
||||
def fan_list(self):
|
||||
"""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):
|
||||
"""Set fan speed."""
|
||||
if mode == SPEED_LOWEST:
|
||||
speed = 0.3
|
||||
elif mode == SPEED_LOW:
|
||||
speed = 0.5
|
||||
if mode == SPEED_LOW:
|
||||
speed = 0.4
|
||||
elif mode == SPEED_MEDIUM:
|
||||
speed = 0.8
|
||||
elif mode == SPEED_HIGH:
|
||||
|
|
|
@ -112,8 +112,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = (
|
||||
round((float(self._current_temperature)), 1))
|
||||
if self._current_temperature is not None:
|
||||
self._target_temperature = (
|
||||
round((float(self._current_temperature)), 1))
|
||||
else:
|
||||
self._target_temperature = round(
|
||||
(float(self.values.primary.data)), 1)
|
||||
|
|
|
@ -47,6 +47,7 @@ class VeraCover(VeraDevice, CoverDevice):
|
|||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
self.vera_device.set_level(position)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
|
@ -60,11 +61,14 @@ class VeraCover(VeraDevice, CoverDevice):
|
|||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self.vera_device.open()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self.vera_device.close()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self.vera_device.stop()
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
@ -20,12 +20,13 @@ _LOGGER = logging.getLogger(__name__)
|
|||
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
|
||||
def get_device(values, **kwargs):
|
||||
def get_device(values, node_config, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
invert_buttons = node_config.get(zwave.CONF_INVERT_OPENCLOSE_BUTTONS)
|
||||
if (values.primary.command_class ==
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||
and values.primary.index == 0):
|
||||
return ZwaveRollershutter(values)
|
||||
return ZwaveRollershutter(values, invert_buttons)
|
||||
elif (values.primary.command_class in [
|
||||
zwave.const.COMMAND_CLASS_SWITCH_BINARY,
|
||||
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]):
|
||||
|
@ -36,13 +37,14 @@ def get_device(values, **kwargs):
|
|||
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Representation of an Zwave roller shutter."""
|
||||
|
||||
def __init__(self, values):
|
||||
def __init__(self, values, invert_buttons):
|
||||
"""Initialize the zwave rollershutter."""
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
# pylint: disable=no-member
|
||||
self._open_id = None
|
||||
self._close_id = None
|
||||
self._current_position = None
|
||||
self._invert_buttons = invert_buttons
|
||||
|
||||
self._workaround = workaround.get_device_mapping(values.primary)
|
||||
if self._workaround:
|
||||
|
@ -56,10 +58,9 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||
|
||||
if self.values.open and self.values.close and \
|
||||
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._close_id = self.values.open.value_id
|
||||
self._workaround = None
|
||||
else:
|
||||
self._open_id = self.values.open.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
|
||||
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
|
||||
url = 'http://api.macvendors.com/' + oui
|
||||
resp = None
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
|
||||
|
@ -570,13 +569,9 @@ class Device(Entity):
|
|||
# in the 'known_devices.yaml' file which only happens
|
||||
# the first time the device is seen.
|
||||
return 'unknown'
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError,
|
||||
aiohttp.errors.ClientDisconnectedError):
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
# same as above
|
||||
return 'unknown'
|
||||
finally:
|
||||
if resp is not None:
|
||||
yield from resp.release()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
|
|
|
@ -18,6 +18,7 @@ from homeassistant.components.device_tracker import ( # NOQA
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
URL = '/api/locative'
|
||||
|
||||
|
||||
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):
|
||||
"""View to handle locative requests."""
|
||||
|
||||
url = '/api/locative'
|
||||
url = URL
|
||||
name = 'api:locative'
|
||||
|
||||
def __init__(self, see):
|
||||
|
|
|
@ -16,11 +16,11 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Setup the MySensors tracker."""
|
||||
def mysensors_callback(gateway, node_id):
|
||||
def mysensors_callback(gateway, msg):
|
||||
"""Callback for mysensors platform."""
|
||||
node = gateway.sensors[node_id]
|
||||
node = gateway.sensors[msg.node_id]
|
||||
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
|
||||
|
||||
pres = gateway.const.Presentation
|
||||
|
@ -37,12 +37,12 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||
'latitude,longitude,altitude', position)
|
||||
continue
|
||||
name = '{} {} {}'.format(
|
||||
node.sketch_name, node_id, child.id)
|
||||
node.sketch_name, msg.node_id, child.id)
|
||||
attr = {
|
||||
mysensors.ATTR_CHILD_ID: child.id,
|
||||
mysensors.ATTR_DESCRIPTION: child.description,
|
||||
mysensors.ATTR_DEVICE: gateway.device,
|
||||
mysensors.ATTR_NODE_ID: node_id,
|
||||
mysensors.ATTR_NODE_ID: msg.node_id,
|
||||
}
|
||||
see(
|
||||
dev_id=slugify(name),
|
||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.util import Throttle
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.3.4']
|
||||
REQUIREMENTS = ['pysnmp==4.3.5']
|
||||
|
||||
CONF_COMMUNITY = 'community'
|
||||
CONF_AUTHKEY = 'authkey'
|
||||
|
@ -125,7 +125,10 @@ class SnmpScanner(DeviceScanner):
|
|||
|
||||
for resrow in restable:
|
||||
for _, val in resrow:
|
||||
mac = binascii.hexlify(val.asOctets()).decode('utf-8')
|
||||
try:
|
||||
mac = binascii.hexlify(val.asOctets()).decode('utf-8')
|
||||
except AttributeError:
|
||||
continue
|
||||
_LOGGER.debug("Found MAC %s", mac)
|
||||
mac = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)])
|
||||
devices.append({'mac': mac})
|
||||
|
|
|
@ -105,8 +105,6 @@ class TadoDeviceScanner(DeviceScanner):
|
|||
_LOGGER.debug("Requesting Tado")
|
||||
|
||||
last_results = []
|
||||
response = None
|
||||
tado_json = None
|
||||
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
|
@ -127,14 +125,10 @@ class TadoDeviceScanner(DeviceScanner):
|
|||
|
||||
tado_json = yield from response.json()
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Cannot load Tado data")
|
||||
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
|
||||
# found under the mobileDevices key.
|
||||
if 'mobileDevices' in tado_json:
|
||||
|
|
|
@ -101,7 +101,6 @@ class UPCDeviceScanner(DeviceScanner):
|
|||
@asyncio.coroutine
|
||||
def async_login(self):
|
||||
"""Login into firmware and get first token."""
|
||||
response = None
|
||||
try:
|
||||
# get first token
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
|
@ -109,7 +108,8 @@ class UPCDeviceScanner(DeviceScanner):
|
|||
"http://{}/common_page/login.html".format(self.host)
|
||||
)
|
||||
|
||||
yield from response.text()
|
||||
yield from response.text()
|
||||
|
||||
self.token = response.cookies['sessionToken'].value
|
||||
|
||||
# login
|
||||
|
@ -119,18 +119,12 @@ class UPCDeviceScanner(DeviceScanner):
|
|||
})
|
||||
|
||||
# successfull?
|
||||
if data is not None:
|
||||
return True
|
||||
return False
|
||||
return data is not None
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Can not load login page from %s", self.host)
|
||||
return False
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_ws_function(self, function, additional_form=None):
|
||||
"""Execute a command on UPC firmware webservice."""
|
||||
|
@ -142,8 +136,7 @@ class UPCDeviceScanner(DeviceScanner):
|
|||
if additional_form:
|
||||
form_data.update(additional_form)
|
||||
|
||||
redirects = True if function != CMD_DEVICES else False
|
||||
response = None
|
||||
redirects = function != CMD_DEVICES
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = yield from self.websession.post(
|
||||
|
@ -163,10 +156,6 @@ class UPCDeviceScanner(DeviceScanner):
|
|||
self.token = response.cookies['sessionToken'].value
|
||||
return (yield from response.text())
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Error on %s", function)
|
||||
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"
|
||||
|
||||
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",
|
||||
os.path.join(STATIC_PATH, "robots.txt"))
|
||||
hass.http.register_static_path("/static", STATIC_PATH)
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
FINGERPRINTS = {
|
||||
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
|
||||
"core.js": "5d08475f03adb5969bd31855d5ca0cfd",
|
||||
"frontend.html": "53c45b837a3bcae7cfb9ef4a5919844f",
|
||||
"mdi.html": "4921d26f29dc148c3e8bd5bcd8ce5822",
|
||||
"frontend.html": "feaf3e9453eca239f29eb10e7645a84f",
|
||||
"mdi.html": "989f02c51eba561dc32b9ecc628a84b3",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-config.html": "6dcb246cd356307a638f81c4f89bf9b3",
|
||||
"panels/ha-panel-dev-event.html": "1f169700c2345785855b1d7919d12326",
|
||||
|
@ -12,9 +12,9 @@ FINGERPRINTS = {
|
|||
"panels/ha-panel-dev-service.html": "0fe8e6acdccf2dc3d1ae657b2c7f2df0",
|
||||
"panels/ha-panel-dev-state.html": "48d37db4a1d6708314ded1d624d0f4d4",
|
||||
"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-logbook.html": "a1fc2b5d739bedb9d87e4da4cd929a71",
|
||||
"panels/ha-panel-map.html": "9aa065b1908089f3bb5af7fdf9485be5",
|
||||
"panels/ha-panel-map.html": "e3c7a54f90dd4269d7e53cdcd96514c6",
|
||||
"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
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
REQUIREMENTS = ["pyhomematic==0.1.22"]
|
||||
REQUIREMENTS = ["pyhomematic==0.1.24"]
|
||||
|
||||
SCAN_INTERVAL_HUB = timedelta(seconds=300)
|
||||
SCAN_INTERVAL_VARIABLES = timedelta(seconds=30)
|
||||
|
@ -82,7 +82,12 @@ HM_ATTRIBUTE_SUPPORT = {
|
|||
'RSSI_DEVICE': ['rssi', {}],
|
||||
'VALVE_STATE': ['valve', {}],
|
||||
'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', {}],
|
||||
'CURRENT': ['current', {}],
|
||||
'VOLTAGE': ['voltage', {}],
|
||||
|
|
|
@ -9,7 +9,6 @@ import json
|
|||
import logging
|
||||
import ssl
|
||||
from ipaddress import ip_network
|
||||
from pathlib import Path
|
||||
|
||||
import os
|
||||
import voluptuous as vol
|
||||
|
@ -31,11 +30,13 @@ from .const import (
|
|||
KEY_USE_X_FORWARDED_FOR, KEY_TRUSTED_NETWORKS,
|
||||
KEY_BANS_ENABLED, KEY_LOGIN_THRESHOLD,
|
||||
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
|
||||
|
||||
REQUIREMENTS = ['aiohttp_cors==0.5.2']
|
||||
|
||||
DOMAIN = 'http'
|
||||
REQUIREMENTS = ('aiohttp_cors==0.5.0',)
|
||||
|
||||
CONF_API_PASSWORD = 'api_password'
|
||||
CONF_SERVER_HOST = 'server_host'
|
||||
|
@ -187,7 +188,7 @@ class HomeAssistantWSGI(object):
|
|||
if is_ban_enabled:
|
||||
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[KEY_USE_X_FORWARDED_FOR] = use_x_forwarded_for
|
||||
self.app[KEY_TRUSTED_NETWORKS] = trusted_networks
|
||||
|
@ -255,31 +256,39 @@ class HomeAssistantWSGI(object):
|
|||
|
||||
self.app.router.add_route('GET', url, redirect)
|
||||
|
||||
def register_static_path(self, url_root, path, cache_length=31):
|
||||
"""Register a folder to serve as a static path.
|
||||
|
||||
Specify optional cache length of asset in days.
|
||||
"""
|
||||
def register_static_path(self, url_path, path, cache_headers=True):
|
||||
"""Register a folder or file to serve as a static 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
|
||||
|
||||
filepath = Path(path)
|
||||
|
||||
@asyncio.coroutine
|
||||
def serve_file(request):
|
||||
"""Serve file from disk."""
|
||||
res = yield from CACHING_FILE_SENDER.send(request, filepath)
|
||||
return res
|
||||
if cache_headers:
|
||||
@asyncio.coroutine
|
||||
def serve_file(request):
|
||||
"""Serve file from disk."""
|
||||
return CachingFileResponse(path)
|
||||
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
|
||||
# to work around cache busting MD5.
|
||||
# Turns something like /static/dev-panel.html into
|
||||
# /static/{filename:dev-panel(-[a-z0-9]{32}|)\.html}
|
||||
base, ext = url_root.rsplit('.', 1)
|
||||
base, file = base.rsplit('/', 1)
|
||||
regex = r"{}(-[a-z0-9]{{32}}|)\.{}".format(file, ext)
|
||||
url_pattern = "{}/{{filename:{}}}".format(base, regex)
|
||||
base, ext = os.path.splitext(url_path)
|
||||
if ext:
|
||||
base, file = base.rsplit('/', 1)
|
||||
regex = r"{}(-[a-z0-9]{{32}}|){}".format(file, ext)
|
||||
url_pattern = "{}/{{filename:{}}}".format(base, regex)
|
||||
else:
|
||||
url_pattern = url_path
|
||||
|
||||
self.app.router.add_route('GET', url_pattern, serve_file)
|
||||
|
||||
|
@ -318,7 +327,7 @@ class HomeAssistantWSGI(object):
|
|||
# re-register all redirects, views, static paths.
|
||||
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:
|
||||
self.server = yield from self.hass.loop.create_server(
|
||||
|
@ -365,8 +374,7 @@ class HomeAssistantView(object):
|
|||
def file(self, request, fil):
|
||||
"""Return a file."""
|
||||
assert isinstance(fil, str), 'only string paths allowed'
|
||||
response = yield from FILE_SENDER.send(request, Path(fil))
|
||||
return response
|
||||
return web.FileResponse(fil)
|
||||
|
||||
def register(self, router):
|
||||
"""Register the view with a router."""
|
||||
|
|
|
@ -3,14 +3,45 @@ import asyncio
|
|||
import re
|
||||
|
||||
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 yarl import unquote
|
||||
|
||||
from .const import KEY_DEVELOPMENT
|
||||
|
||||
_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."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -20,46 +51,34 @@ class CachingFileSender(FileSender):
|
|||
orig_sendfile = self._sendfile
|
||||
|
||||
@asyncio.coroutine
|
||||
def sendfile(request, resp, fobj, count):
|
||||
def sendfile(request, fobj, count):
|
||||
"""Sendfile that includes a cache header."""
|
||||
if not request.app[KEY_DEVELOPMENT]:
|
||||
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)
|
||||
|
||||
yield from orig_sendfile(request, resp, fobj, count)
|
||||
yield from orig_sendfile(request, fobj, count)
|
||||
|
||||
# Overwriting like this because __init__ can change implementation.
|
||||
self._sendfile = sendfile
|
||||
|
||||
|
||||
FILE_SENDER = FileSender()
|
||||
CACHING_FILE_SENDER = CachingFileSender()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def staticresource_middleware(app, handler):
|
||||
"""Enhance StaticResourceHandler middleware.
|
||||
|
||||
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
|
||||
|
||||
"""Middleware to strip out fingerprint from fingerprinted assets."""
|
||||
@asyncio.coroutine
|
||||
def static_middleware_handler(request):
|
||||
"""Strip out fingerprints from resource names."""
|
||||
if not request.path.startswith('/static/'):
|
||||
return handler(request)
|
||||
|
||||
fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
|
||||
|
||||
if fingerprinted:
|
||||
request.match_info['filename'] = \
|
||||
'{}.{}'.format(*fingerprinted.groups())
|
||||
|
||||
resp = yield from handler(request)
|
||||
return resp
|
||||
return handler(request)
|
||||
|
||||
return static_middleware_handler
|
||||
|
|
|
@ -112,8 +112,6 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity):
|
|||
|
||||
params['image_bytes'] = str(b64encode(image), 'utf-8')
|
||||
|
||||
data = None
|
||||
request = None
|
||||
try:
|
||||
with async_timeout.timeout(self.timeout, loop=self.hass.loop):
|
||||
request = yield from websession.post(
|
||||
|
@ -127,14 +125,10 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity):
|
|||
request.status, data.get('error'))
|
||||
return
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Timeout for openalpr api.")
|
||||
return
|
||||
|
||||
finally:
|
||||
if request is not None:
|
||||
yield from request.release()
|
||||
|
||||
# processing api data
|
||||
vehicles = 0
|
||||
result = {}
|
||||
|
|
|
@ -30,13 +30,11 @@ SERVICE_SCHEMA = vol.Schema({
|
|||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
DEFAULT_CONFIG = {CONF_INITIAL: DEFAULT_INITIAL}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: vol.Any({
|
||||
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,
|
||||
}, None)
|
||||
})
|
||||
|
@ -72,13 +70,13 @@ def async_setup(hass, config):
|
|||
|
||||
for object_id, cfg in config[DOMAIN].items():
|
||||
if not cfg:
|
||||
cfg = DEFAULT_CONFIG
|
||||
cfg = {}
|
||||
|
||||
name = cfg.get(CONF_NAME)
|
||||
state = cfg.get(CONF_INITIAL)
|
||||
initial = cfg.get(CONF_INITIAL)
|
||||
icon = cfg.get(CONF_ICON)
|
||||
|
||||
entities.append(InputBoolean(object_id, name, state, icon))
|
||||
entities.append(InputBoolean(object_id, name, initial, icon))
|
||||
|
||||
if not entities:
|
||||
return False
|
||||
|
@ -113,11 +111,11 @@ def async_setup(hass, config):
|
|||
class InputBoolean(ToggleEntity):
|
||||
"""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."""
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self._name = name
|
||||
self._state = state
|
||||
self._state = initial
|
||||
self._icon = icon
|
||||
|
||||
@property
|
||||
|
@ -143,10 +141,12 @@ class InputBoolean(ToggleEntity):
|
|||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Called when entity about to be added to hass."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if not state:
|
||||
# If not None, we got an initial value.
|
||||
if self._state is not None:
|
||||
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
|
||||
def async_turn_on(self, **kwargs):
|
||||
|
|
|
@ -58,10 +58,10 @@ SERVICE_SET_OPTIONS_SCHEMA = vol.Schema({
|
|||
def _cv_input_select(cfg):
|
||||
"""Config validation helper for input select (Voluptuous)."""
|
||||
options = cfg[CONF_OPTIONS]
|
||||
state = cfg.get(CONF_INITIAL, options[0])
|
||||
if state not in options:
|
||||
initial = cfg.get(CONF_INITIAL)
|
||||
if initial is not None and initial not in options:
|
||||
raise vol.Invalid('initial state "{}" is not part of the options: {}'
|
||||
.format(state, ','.join(options)))
|
||||
.format(initial, ','.join(options)))
|
||||
return cfg
|
||||
|
||||
|
||||
|
@ -117,9 +117,9 @@ def async_setup(hass, config):
|
|||
for object_id, cfg in config[DOMAIN].items():
|
||||
name = cfg.get(CONF_NAME)
|
||||
options = cfg.get(CONF_OPTIONS)
|
||||
state = cfg.get(CONF_INITIAL, options[0])
|
||||
initial = cfg.get(CONF_INITIAL)
|
||||
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:
|
||||
return False
|
||||
|
@ -187,23 +187,25 @@ def async_setup(hass, config):
|
|||
class InputSelect(Entity):
|
||||
"""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."""
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self._name = name
|
||||
self._current_option = state
|
||||
self._current_option = initial
|
||||
self._options = options
|
||||
self._icon = icon
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""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)
|
||||
if not state:
|
||||
return
|
||||
if state.state not in self._options:
|
||||
return
|
||||
self._current_option = state.state
|
||||
if not state or state.state not in self._options:
|
||||
self._current_option = self._options[0]
|
||||
else:
|
||||
self._current_option = state.state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
|
|
@ -45,11 +45,10 @@ def _cv_input_slider(cfg):
|
|||
if minimum >= maximum:
|
||||
raise vol.Invalid('Maximum ({}) is not greater than minimum ({})'
|
||||
.format(minimum, maximum))
|
||||
state = cfg.get(CONF_INITIAL, minimum)
|
||||
if state < minimum or state > maximum:
|
||||
state = cfg.get(CONF_INITIAL)
|
||||
if state is not None and (state < minimum or state > maximum):
|
||||
raise vol.Invalid('Initial value {} not in range {}-{}'
|
||||
.format(state, minimum, maximum))
|
||||
cfg[CONF_INITIAL] = state
|
||||
return cfg
|
||||
|
||||
|
||||
|
@ -88,12 +87,12 @@ def async_setup(hass, config):
|
|||
name = cfg.get(CONF_NAME)
|
||||
minimum = cfg.get(CONF_MIN)
|
||||
maximum = cfg.get(CONF_MAX)
|
||||
state = cfg.get(CONF_INITIAL, minimum)
|
||||
initial = cfg.get(CONF_INITIAL)
|
||||
step = cfg.get(CONF_STEP)
|
||||
icon = cfg.get(CONF_ICON)
|
||||
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))
|
||||
|
||||
if not entities:
|
||||
|
@ -120,12 +119,12 @@ def async_setup(hass, config):
|
|||
class InputSlider(Entity):
|
||||
"""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):
|
||||
"""Initialize a select input."""
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self._name = name
|
||||
self._current_value = state
|
||||
self._current_value = initial
|
||||
self._minimum = minimum
|
||||
self._maximum = maximum
|
||||
self._step = step
|
||||
|
@ -169,14 +168,17 @@ class InputSlider(Entity):
|
|||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Called when entity about to be added to hass."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if not state:
|
||||
if self._current_value is not None:
|
||||
return
|
||||
|
||||
num_value = float(state.state)
|
||||
if num_value < self._minimum or num_value > self._maximum:
|
||||
return
|
||||
self._current_value = num_value
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
value = state and float(state.state)
|
||||
|
||||
# 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
|
||||
def async_select_value(self, value):
|
||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.components.light import (
|
|||
PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['flux_led==0.15']
|
||||
REQUIREMENTS = ['flux_led==0.18']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
|
|||
|
||||
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_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP)
|
||||
SUPPORT_HUE_COLOR = (SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT |
|
||||
|
@ -58,6 +58,7 @@ SUPPORT_HUE = {
|
|||
'Extended color light': SUPPORT_HUE_EXTENDED,
|
||||
'Color light': SUPPORT_HUE_COLOR,
|
||||
'Dimmable light': SUPPORT_HUE_DIMMABLE,
|
||||
'On/Off plug-in unit': SUPPORT_HUE_ON_OFF,
|
||||
'Color temperature light': SUPPORT_HUE_COLOR_TEMP
|
||||
}
|
||||
|
||||
|
|
|
@ -175,7 +175,7 @@ class InsteonLocalDimmerDevice(Light):
|
|||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = int(kwargs[ATTR_BRIGHTNESS]) / 255 * 100
|
||||
|
||||
self.node.on(brightness)
|
||||
self.node.change_level(brightness)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""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)
|
||||
)
|
||||
|
||||
hass.async_add_job(async_add_devices(device_list))
|
||||
async_add_devices(device_list)
|
||||
|
||||
|
||||
class InsteonPLMDimmerDevice(Light):
|
||||
|
|
|
@ -7,6 +7,7 @@ https://home-assistant.io/components/light.lifx/
|
|||
import colorsys
|
||||
import logging
|
||||
import asyncio
|
||||
import sys
|
||||
from functools import partial
|
||||
from datetime import timedelta
|
||||
|
||||
|
@ -51,6 +52,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
"""Setup the LIFX platform."""
|
||||
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)
|
||||
|
||||
lifx_manager = LIFXManager(hass, async_add_devices)
|
||||
|
@ -90,7 +95,7 @@ class LIFXManager(object):
|
|||
entity = LIFXLight(device)
|
||||
_LOGGER.debug("%s register READY", entity.ipaddr)
|
||||
self.entities[device.mac_addr] = entity
|
||||
self.hass.async_add_job(self.async_add_devices, [entity])
|
||||
self.async_add_devices([entity])
|
||||
|
||||
@callback
|
||||
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_NUMBER = 'number'
|
||||
CONF_VERSION = 'version'
|
||||
CONF_BRIDGE_LED = 'bridge_led'
|
||||
|
||||
DEFAULT_LED_TYPE = 'rgbw'
|
||||
DEFAULT_PORT = 5987
|
||||
DEFAULT_TRANSITION = 0
|
||||
DEFAULT_VERSION = 6
|
||||
|
||||
LED_TYPE = ['rgbw', 'rgbww', 'white']
|
||||
LED_TYPE = ['rgbw', 'rgbww', 'white', 'bridge-led']
|
||||
|
||||
RGB_BOUNDARY = 40
|
||||
|
||||
|
@ -54,7 +53,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.Optional(CONF_VERSION,
|
||||
default=DEFAULT_VERSION): cv.positive_int,
|
||||
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_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_TYPE, DEFAULT_LED_TYPE))
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ class LiteJetLight(Light):
|
|||
def _on_load_changed(self):
|
||||
"""Called on a LiteJet thread when a load's state changes."""
|
||||
_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
|
||||
def supported_features(self):
|
||||
|
|
|
@ -19,7 +19,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
"""Setup Lutron Caseta lights."""
|
||||
devs = []
|
||||
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:
|
||||
dev = LutronCasetaLight(light_device, bridge)
|
||||
devs.append(dev)
|
||||
|
|
|
@ -148,16 +148,12 @@ class MqttJson(Light):
|
|||
self._flash_times = flash_times
|
||||
|
||||
self._supported_features = (SUPPORT_TRANSITION | SUPPORT_FLASH)
|
||||
self._supported_features |= (rgb is not None and SUPPORT_RGB_COLOR)
|
||||
self._supported_features |= (brightness is not None and
|
||||
SUPPORT_BRIGHTNESS)
|
||||
self._supported_features |= (color_temp is not None and
|
||||
SUPPORT_COLOR_TEMP)
|
||||
self._supported_features |= (effect is not None and
|
||||
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)
|
||||
self._supported_features |= (rgb and SUPPORT_RGB_COLOR)
|
||||
self._supported_features |= (brightness and SUPPORT_BRIGHTNESS)
|
||||
self._supported_features |= (color_temp and SUPPORT_COLOR_TEMP)
|
||||
self._supported_features |= (effect and SUPPORT_EFFECT)
|
||||
self._supported_features |= (white_value and SUPPORT_WHITE_VALUE)
|
||||
self._supported_features |= (xy and SUPPORT_XY_COLOR)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
|
|
|
@ -9,15 +9,12 @@ import subprocess
|
|||
|
||||
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 (
|
||||
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS,
|
||||
ATTR_RGB_COLOR, SUPPORT_RGB_COLOR,
|
||||
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR,
|
||||
Light, PLATFORM_SCHEMA)
|
||||
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']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -32,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Piglow Light platform."""
|
||||
"""Set up the Piglow Light platform."""
|
||||
import piglow
|
||||
|
||||
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)
|
||||
|
||||
# Add devices
|
||||
add_devices([PiglowLight(piglow, name)])
|
||||
|
||||
|
||||
|
|
|
@ -10,12 +10,14 @@ import logging
|
|||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
|
||||
from homeassistant.components.rflink import (
|
||||
CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT,
|
||||
CONF_IGNORE_DEVICES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER,
|
||||
DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA, DOMAIN,
|
||||
EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, vol)
|
||||
CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICE_DEFAULTS, CONF_DEVICES,
|
||||
CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASSES, CONF_IGNORE_DEVICES,
|
||||
CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER,
|
||||
DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA,
|
||||
DOMAIN, EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, vol)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_PLATFORM, CONF_TYPE, STATE_UNKNOWN)
|
||||
|
||||
DEPENDENCIES = ['rflink']
|
||||
|
||||
_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_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})):
|
||||
DEVICE_DEFAULTS_SCHEMA,
|
||||
vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean,
|
||||
vol.Optional(CONF_DEVICES, default={}): vol.Schema({
|
||||
cv.string: {
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
|
@ -38,8 +41,13 @@ PLATFORM_SCHEMA = vol.Schema({
|
|||
TYPE_HYBRID, TYPE_TOGGLE),
|
||||
vol.Optional(CONF_ALIASSES, default=[]):
|
||||
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_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)
|
||||
|
||||
# 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][
|
||||
EVENT_KEY_COMMAND][_id].append(device)
|
||||
|
||||
|
@ -138,10 +163,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
hass.data[DATA_ENTITY_LOOKUP][
|
||||
EVENT_KEY_COMMAND][device_id].append(device)
|
||||
|
||||
# Make sure the event is processed by the new entity
|
||||
device.handle_event(event)
|
||||
# Schedule task to process event after entity is created
|
||||
hass.async_add_job(device.handle_event, event)
|
||||
|
||||
hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_COMMAND] = add_new_device
|
||||
if config[CONF_AUTOMATIC_ADD]:
|
||||
hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_COMMAND] = add_new_device
|
||||
|
||||
|
||||
class RflinkLight(SwitchableRflinkDevice, Light):
|
||||
|
|
|
@ -8,7 +8,6 @@ import logging
|
|||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS)
|
||||
from homeassistant.const import (STATE_OFF, STATE_ON)
|
||||
from homeassistant.components.vera import (
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
|
@ -53,13 +52,13 @@ class VeraLight(VeraDevice, Light):
|
|||
else:
|
||||
self.vera_device.switch_on()
|
||||
|
||||
self._state = STATE_ON
|
||||
self._state = True
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
self.vera_device.switch_off()
|
||||
self._state = STATE_OFF
|
||||
self._state = False
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
|
|
|
@ -44,13 +44,14 @@ DEVICE_SCHEMA = vol.Schema({
|
|||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, })
|
||||
|
||||
SUPPORT_YEELIGHT_RGB = (SUPPORT_RGB_COLOR |
|
||||
SUPPORT_COLOR_TEMP)
|
||||
|
||||
SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS |
|
||||
SUPPORT_TRANSITION |
|
||||
SUPPORT_FLASH)
|
||||
|
||||
SUPPORT_YEELIGHT_RGB = (SUPPORT_YEELIGHT |
|
||||
SUPPORT_RGB_COLOR |
|
||||
SUPPORT_COLOR_TEMP)
|
||||
|
||||
|
||||
def _cmd(func):
|
||||
"""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.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
|
||||
except yeelight.BulbException as ex:
|
||||
self._available = False
|
||||
|
@ -203,6 +201,9 @@ class YeelightLight(Light):
|
|||
try:
|
||||
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"
|
||||
|
||||
bright = self._properties.get("bright", None)
|
||||
|
@ -254,7 +255,8 @@ class YeelightLight(Light):
|
|||
def set_flash(self, flash) -> None:
|
||||
"""Activate flash."""
|
||||
if flash:
|
||||
from yeelight import RGBTransition, SleepTransition, Flow
|
||||
from yeelight import (RGBTransition, SleepTransition, Flow,
|
||||
BulbException)
|
||||
if self._bulb.last_properties["color_mode"] != 1:
|
||||
_LOGGER.error("Flash supported currently only in RGB mode.")
|
||||
return
|
||||
|
@ -279,10 +281,14 @@ class YeelightLight(Light):
|
|||
duration=duration))
|
||||
|
||||
flow = Flow(count=count, transitions=transitions)
|
||||
self._bulb.start_flow(flow)
|
||||
try:
|
||||
self._bulb.start_flow(flow)
|
||||
except BulbException as ex:
|
||||
_LOGGER.error("Unable to set flash: %s", ex)
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Turn the bulb on."""
|
||||
import yeelight
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
colortemp = kwargs.get(ATTR_COLOR_TEMP)
|
||||
rgb = kwargs.get(ATTR_RGB_COLOR)
|
||||
|
@ -292,22 +298,43 @@ class YeelightLight(Light):
|
|||
if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
|
||||
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
|
||||
|
||||
self._bulb.turn_on(duration=duration)
|
||||
try:
|
||||
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:
|
||||
self.set_music_mode(self.config[CONF_MODE_MUSIC])
|
||||
try:
|
||||
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)
|
||||
|
||||
# values checked for none in methods
|
||||
self.set_rgb(rgb, duration)
|
||||
self.set_colortemp(colortemp, duration)
|
||||
self.set_brightness(brightness, duration)
|
||||
self.set_flash(flash)
|
||||
try:
|
||||
# values checked for none in methods
|
||||
self.set_rgb(rgb, duration)
|
||||
self.set_colortemp(colortemp, duration)
|
||||
self.set_brightness(brightness, duration)
|
||||
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.
|
||||
if self.config[CONF_SAVE_ON_CHANGE]:
|
||||
if brightness or colortemp or rgb:
|
||||
if self.config[CONF_SAVE_ON_CHANGE] and (brightness
|
||||
or colortemp
|
||||
or rgb):
|
||||
try:
|
||||
self.set_default()
|
||||
except yeelight.BulbException as ex:
|
||||
_LOGGER.error("Unable to set the defaults: %s", ex)
|
||||
return
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Turn off."""
|
||||
self._bulb.turn_off()
|
||||
import yeelight
|
||||
try:
|
||||
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/
|
||||
"""
|
||||
import logging
|
||||
|
||||
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
|
||||
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']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
@ -35,7 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
hub = yeelightsunflower.Hub(host)
|
||||
|
||||
if not hub.available:
|
||||
_LOGGER.error('Could not connect to Yeelight Sunflower hub')
|
||||
_LOGGER.error("Could not connect to Yeelight Sunflower hub")
|
||||
return False
|
||||
|
||||
add_devices(SunflowerBulb(light) for light in hub.get_lights())
|
||||
|
@ -55,7 +53,7 @@ class SunflowerBulb(Light):
|
|||
@property
|
||||
def name(self):
|
||||
"""Return the display name of this light."""
|
||||
return "sunflower_{}".format(self._light.zid)
|
||||
return 'sunflower_{}'.format(self._light.zid)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
|
|
|
@ -10,8 +10,8 @@ import logging
|
|||
# pylint: disable=import-error
|
||||
from threading import Timer
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
|
||||
ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \
|
||||
SUPPORT_RGB_COLOR, DOMAIN, Light
|
||||
ATTR_RGB_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \
|
||||
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, DOMAIN, Light
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
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_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):
|
||||
"""Create zwave entity device."""
|
||||
|
@ -72,6 +67,13 @@ def brightness_state(value):
|
|||
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):
|
||||
"""Representation of a Z-Wave dimmer."""
|
||||
|
||||
|
@ -80,6 +82,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self._brightness = None
|
||||
self._state = None
|
||||
self._supported_features = None
|
||||
self._delay = delay
|
||||
self._refresh_value = refresh
|
||||
self._zw098 = None
|
||||
|
@ -100,6 +103,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||
self._timer = None
|
||||
_LOGGER.debug('self._refreshing=%s self.delay=%s',
|
||||
self._refresh_value, self._delay)
|
||||
self.value_added()
|
||||
self.update_properties()
|
||||
|
||||
def update_properties(self):
|
||||
|
@ -107,6 +111,12 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||
# Brightness
|
||||
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):
|
||||
"""Called when a value for this entity's node has changed."""
|
||||
if self._refresh_value:
|
||||
|
@ -139,10 +149,43 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||
@property
|
||||
def supported_features(self):
|
||||
"""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):
|
||||
"""Turn the device on."""
|
||||
self._set_duration(**kwargs)
|
||||
|
||||
# Zwave multilevel switches use a range of [0, 99] to control
|
||||
# brightness. Level 255 means to set it to previous value.
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
|
@ -156,17 +199,12 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
self._set_duration(**kwargs)
|
||||
|
||||
if self.node.set_dimmer(self.values.primary.value_id, 0):
|
||||
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):
|
||||
"""Representation of a Z-Wave color changing light."""
|
||||
|
||||
|
@ -178,6 +216,14 @@ class ZwaveColorLight(ZwaveDimmer):
|
|||
|
||||
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):
|
||||
"""Update internal properties based on zwave values."""
|
||||
super().update_properties()
|
||||
|
@ -288,11 +334,3 @@ class ZwaveColorLight(ZwaveDimmer):
|
|||
self.values.color.data = rgbw
|
||||
|
||||
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',
|
||||
'18': 'Locked with Keypad by user ',
|
||||
'19': 'Unlocked with Keypad by user ',
|
||||
'21': 'Manually Locked by ',
|
||||
'22': 'Manually Unlocked by Key or Inside thumb turn',
|
||||
'21': 'Manually Locked ',
|
||||
'22': 'Manually Unlocked ',
|
||||
'24': 'Locked by RF',
|
||||
'25': 'Unlocked by RF',
|
||||
'27': 'Auto re-lock',
|
||||
|
@ -69,8 +69,8 @@ LOCK_ALARM_TYPE = {
|
|||
}
|
||||
|
||||
MANUAL_LOCK_ALARM_LEVEL = {
|
||||
'1': 'Key Cylinder or Inside thumb turn',
|
||||
'2': 'Touch function (lock and leave)'
|
||||
'1': 'by Key Cylinder or Inside thumb turn',
|
||||
'2': 'by Touch function (lock and leave)'
|
||||
}
|
||||
|
||||
TAMPER_ALARM_LEVEL = {
|
||||
|
|
|
@ -17,8 +17,8 @@ from homeassistant.helpers import discovery
|
|||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['https://github.com/gurumitts/'
|
||||
'pylutron-caseta/archive/v0.2.4.zip#'
|
||||
'pylutron-caseta==v0.2.4']
|
||||
'pylutron-caseta/archive/v0.2.5.zip#'
|
||||
'pylutron-caseta==v0.2.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -864,21 +864,17 @@ def _async_fetch_image(hass, url):
|
|||
|
||||
content, content_type = (None, None)
|
||||
websession = async_get_clientsession(hass)
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=hass.loop):
|
||||
response = yield from websession.get(url)
|
||||
if response.status == 200:
|
||||
content = yield from response.read()
|
||||
content_type = response.headers.get(CONTENT_TYPE_HEADER)
|
||||
|
||||
if response.status == 200:
|
||||
content = yield from response.read()
|
||||
content_type = response.headers.get(CONTENT_TYPE_HEADER)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
if not content:
|
||||
return (None, None)
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ from homeassistant.core import callback
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['pyemby==1.1']
|
||||
REQUIREMENTS = ['pyemby==1.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Frontier Silicon platform."""
|
||||
"""Set up the Frontier Silicon platform."""
|
||||
import requests
|
||||
|
||||
if discovery_info is not None:
|
||||
|
@ -59,10 +59,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
add_devices(
|
||||
[FSAPIDevice(DEVICE_URL.format(host, port), password)],
|
||||
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
|
||||
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)
|
||||
|
||||
return False
|
||||
|
|
|
@ -27,7 +27,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
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__)
|
||||
|
||||
|
|
|
@ -14,8 +14,9 @@ from homeassistant.components.media_player import (
|
|||
from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['https://github.com/danieljkemp/onkyo-eiscp/archive/'
|
||||
'python3.zip#onkyo-eiscp==0.9.2']
|
||||
REQUIREMENTS = ['https://github.com/miracle2k/onkyo-eiscp/archive/'
|
||||
'066023aec04770518d494c32fb72eea0ec5c1b7c.zip#'
|
||||
'onkyo-eiscp==1.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -127,7 +128,7 @@ class OnkyoDevice(MediaPlayerDevice):
|
|||
self._current_source = '_'.join(
|
||||
[i for i in current_source_tuples[1]])
|
||||
self._muted = bool(mute_raw[1] == 'on')
|
||||
self._volume = int(volume_raw[1], 16) / 80.0
|
||||
self._volume = volume_raw[1] / 80.0
|
||||
|
||||
@property
|
||||
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
|
||||
https://home-assistant.io/components/media_player.sonos/
|
||||
"""
|
||||
import asyncio
|
||||
import datetime
|
||||
import functools as ft
|
||||
import logging
|
||||
from os import path
|
||||
import socket
|
||||
import urllib
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
|
@ -107,7 +110,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
return
|
||||
|
||||
if player.is_visible:
|
||||
device = SonosDevice(hass, player)
|
||||
device = SonosDevice(player)
|
||||
add_devices([device], True)
|
||||
hass.data[DATA_SONOS].append(device)
|
||||
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.')
|
||||
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)
|
||||
_LOGGER.info('Added %s Sonos speakers', len(players))
|
||||
|
||||
|
@ -216,19 +219,42 @@ class _ProcessSonosEventQueue():
|
|||
def _get_entity_from_soco(hass, soco):
|
||||
"""Return SonosDevice from SoCo."""
|
||||
for device in hass.data[DATA_SONOS]:
|
||||
if soco == device.soco_device:
|
||||
if soco == device.soco:
|
||||
return 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):
|
||||
"""Representation of a Sonos device."""
|
||||
|
||||
def __init__(self, hass, player):
|
||||
def __init__(self, player):
|
||||
"""Initialize the Sonos device."""
|
||||
from soco.snapshot import Snapshot
|
||||
|
||||
self.hass = hass
|
||||
self.volume_increment = 5
|
||||
self._unique_id = player.uid
|
||||
self._player = player
|
||||
|
@ -260,9 +286,14 @@ class SonosDevice(MediaPlayerDevice):
|
|||
self._is_playing_tv = None
|
||||
self._favorite_sources = None
|
||||
self._source_name = None
|
||||
self.soco_snapshot = Snapshot(self._player)
|
||||
self._soco_snapshot = 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
|
||||
def should_poll(self):
|
||||
"""Polling needed."""
|
||||
|
@ -297,7 +328,7 @@ class SonosDevice(MediaPlayerDevice):
|
|||
return self._coordinator is None
|
||||
|
||||
@property
|
||||
def soco_device(self):
|
||||
def soco(self):
|
||||
"""Return soco device."""
|
||||
return self._player
|
||||
|
||||
|
@ -327,7 +358,6 @@ class SonosDevice(MediaPlayerDevice):
|
|||
auto_renew=True,
|
||||
event_queue=self._queue)
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-statements
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
if self._speaker_info is None:
|
||||
|
@ -606,16 +636,6 @@ class SonosDevice(MediaPlayerDevice):
|
|||
self._is_playing_tv = is_playing_tv
|
||||
self._is_playing_line_in = is_playing_line_in
|
||||
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
|
||||
|
||||
def _format_media_image_url(self, url, fallback_uri):
|
||||
|
@ -781,27 +801,31 @@ class SonosDevice(MediaPlayerDevice):
|
|||
|
||||
return supported
|
||||
|
||||
@soco_error
|
||||
def volume_up(self):
|
||||
"""Volume up media player."""
|
||||
self._player.volume += self.volume_increment
|
||||
|
||||
@soco_error
|
||||
def volume_down(self):
|
||||
"""Volume down media player."""
|
||||
self._player.volume -= self.volume_increment
|
||||
|
||||
@soco_error
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
self._player.volume = str(int(volume * 100))
|
||||
|
||||
@soco_error
|
||||
def mute_volume(self, mute):
|
||||
"""Mute (true) or unmute (false) media player."""
|
||||
self._player.mute = mute
|
||||
|
||||
@soco_error
|
||||
@soco_coordinator
|
||||
def select_source(self, source):
|
||||
"""Select input source."""
|
||||
if self._coordinator:
|
||||
self._coordinator.select_source(source)
|
||||
elif source == SUPPORT_SOURCE_LINEIN:
|
||||
if source == SUPPORT_SOURCE_LINEIN:
|
||||
self._source_name = SUPPORT_SOURCE_LINEIN
|
||||
self._player.switch_to_line_in()
|
||||
elif source == SUPPORT_SOURCE_TV:
|
||||
|
@ -842,83 +866,78 @@ class SonosDevice(MediaPlayerDevice):
|
|||
else:
|
||||
return self._source_name
|
||||
|
||||
@soco_error
|
||||
def turn_off(self):
|
||||
"""Turn off media player."""
|
||||
self.media_pause()
|
||||
|
||||
@soco_error
|
||||
@soco_coordinator
|
||||
def media_play(self):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""Turn the media player on."""
|
||||
self.media_play()
|
||||
|
||||
@soco_error
|
||||
@soco_coordinator
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""
|
||||
Send the play_media command to the media player.
|
||||
|
||||
If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
|
||||
"""
|
||||
if self._coordinator:
|
||||
self._coordinator.play_media(media_type, media_id, **kwargs)
|
||||
if kwargs.get(ATTR_MEDIA_ENQUEUE):
|
||||
from soco.exceptions import SoCoUPnPException
|
||||
try:
|
||||
self._player.add_uri_to_queue(media_id)
|
||||
except SoCoUPnPException:
|
||||
_LOGGER.error('Error parsing media uri "%s", '
|
||||
"please check it's a valid media resource "
|
||||
'supported by Sonos', media_id)
|
||||
else:
|
||||
if kwargs.get(ATTR_MEDIA_ENQUEUE):
|
||||
from soco.exceptions import SoCoUPnPException
|
||||
try:
|
||||
self._player.add_uri_to_queue(media_id)
|
||||
except SoCoUPnPException:
|
||||
_LOGGER.error('Error parsing media uri "%s", '
|
||||
"please check it's a valid media resource "
|
||||
'supported by Sonos', media_id)
|
||||
else:
|
||||
self._player.play_uri(media_id)
|
||||
self._player.play_uri(media_id)
|
||||
|
||||
@soco_error
|
||||
def join(self, master):
|
||||
"""Join the player to a group."""
|
||||
coord = [device for device in self.hass.data[DATA_SONOS]
|
||||
|
@ -926,29 +945,26 @@ class SonosDevice(MediaPlayerDevice):
|
|||
|
||||
if coord and master != self.entity_id:
|
||||
coord = coord[0]
|
||||
if coord.soco_device.group.coordinator != coord.soco_device:
|
||||
coord.soco_device.unjoin()
|
||||
self._player.join(coord.soco_device)
|
||||
if coord.soco.group.coordinator != coord.soco:
|
||||
coord.soco.unjoin()
|
||||
self._player.join(coord.soco)
|
||||
self._coordinator = coord
|
||||
else:
|
||||
_LOGGER.error("Master not found %s", master)
|
||||
|
||||
@soco_error
|
||||
def unjoin(self):
|
||||
"""Unjoin the player from a group."""
|
||||
self._player.unjoin()
|
||||
self._coordinator = None
|
||||
|
||||
@soco_error
|
||||
def snapshot(self, with_group=True):
|
||||
"""Snapshot the player."""
|
||||
from soco.exceptions import SoCoException
|
||||
try:
|
||||
self.soco_snapshot.is_playing_queue = False
|
||||
self.soco_snapshot.is_coordinator = False
|
||||
self.soco_snapshot.snapshot()
|
||||
except SoCoException:
|
||||
_LOGGER.debug("Error on snapshot %s", self.entity_id)
|
||||
self._snapshot_group = None
|
||||
return
|
||||
from soco.snapshot import Snapshot
|
||||
|
||||
self._soco_snapshot = Snapshot(self._player)
|
||||
self._soco_snapshot.snapshot()
|
||||
|
||||
if with_group:
|
||||
self._snapshot_group = self._player.group
|
||||
|
@ -957,14 +973,15 @@ class SonosDevice(MediaPlayerDevice):
|
|||
else:
|
||||
self._snapshot_group = None
|
||||
|
||||
@soco_error
|
||||
def restore(self, with_group=True):
|
||||
"""Restore snapshot for the player."""
|
||||
from soco.exceptions import SoCoException
|
||||
try:
|
||||
# need catch exception if a coordinator is going to slave.
|
||||
# this state will recover with group part.
|
||||
self.soco_snapshot.restore(False)
|
||||
except (TypeError, SoCoException):
|
||||
self._soco_snapshot.restore(False)
|
||||
except (TypeError, AttributeError, SoCoException):
|
||||
_LOGGER.debug("Error on restore %s", self.entity_id)
|
||||
|
||||
# restore groups
|
||||
|
@ -1006,19 +1023,17 @@ class SonosDevice(MediaPlayerDevice):
|
|||
if s_dev != old.coordinator:
|
||||
s_dev.join(old.coordinator)
|
||||
|
||||
@soco_error
|
||||
@soco_coordinator
|
||||
def set_sleep_timer(self, sleep_time):
|
||||
"""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):
|
||||
"""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
|
||||
def device_state_attributes(self):
|
||||
|
|
|
@ -117,7 +117,6 @@ class LogitechMediaServer(object):
|
|||
@asyncio.coroutine
|
||||
def async_query(self, *command, player=""):
|
||||
"""Abstract out the JSON-RPC connection."""
|
||||
response = None
|
||||
auth = None if self._username is None else aiohttp.BasicAuth(
|
||||
self._username, self._password)
|
||||
url = "http://{}:{}/jsonrpc.js".format(
|
||||
|
@ -138,22 +137,17 @@ class LogitechMediaServer(object):
|
|||
data=data,
|
||||
auth=auth)
|
||||
|
||||
if response.status == 200:
|
||||
data = yield from response.json()
|
||||
else:
|
||||
if response.status != 200:
|
||||
_LOGGER.error(
|
||||
"Query failed, response code: %s Full message: %s",
|
||||
response.status, response)
|
||||
return False
|
||||
|
||||
except (asyncio.TimeoutError,
|
||||
aiohttp.errors.ClientError,
|
||||
aiohttp.errors.ClientDisconnectedError) as error:
|
||||
data = yield from response.json()
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError) as error:
|
||||
_LOGGER.error("Failed communicating with LMS: %s", type(error))
|
||||
return False
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
try:
|
||||
return data['result']
|
||||
|
|
|
@ -425,12 +425,12 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
|||
return self._async_call_service(
|
||||
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.
|
||||
|
||||
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(
|
||||
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