Merge pull request #6956 from home-assistant/release-0-42

0.42
pull/6993/head 0.42
Paulus Schoutsen 2017-04-08 15:40:30 -07:00 committed by GitHub
commit cd8723f742
224 changed files with 5773 additions and 3272 deletions

View File

@ -152,6 +152,7 @@ omit =
homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/alarm_control_panel/simplisafe.py homeassistant/components/alarm_control_panel/simplisafe.py
homeassistant/components/alarm_control_panel/totalconnect.py
homeassistant/components/apiai.py homeassistant/components/apiai.py
homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/arest.py
homeassistant/components/binary_sensor/concord232.py homeassistant/components/binary_sensor/concord232.py
@ -225,6 +226,7 @@ omit =
homeassistant/components/light/hue.py homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py homeassistant/components/light/hyperion.py
homeassistant/components/light/lifx.py homeassistant/components/light/lifx.py
homeassistant/components/light/lifx_legacy.py
homeassistant/components/light/limitlessled.py homeassistant/components/light/limitlessled.py
homeassistant/components/light/osramlightify.py homeassistant/components/light/osramlightify.py
homeassistant/components/light/tikteck.py homeassistant/components/light/tikteck.py
@ -235,6 +237,7 @@ omit =
homeassistant/components/light/zengge.py homeassistant/components/light/zengge.py
homeassistant/components/lirc.py homeassistant/components/lirc.py
homeassistant/components/lock/nuki.py homeassistant/components/lock/nuki.py
homeassistant/components/lock/lockitron.py
homeassistant/components/media_player/anthemav.py homeassistant/components/media_player/anthemav.py
homeassistant/components/media_player/apple_tv.py homeassistant/components/media_player/apple_tv.py
homeassistant/components/media_player/aquostv.py homeassistant/components/media_player/aquostv.py
@ -323,6 +326,7 @@ omit =
homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/coinmarketcap.py
homeassistant/components/sensor/comed_hourly_pricing.py homeassistant/components/sensor/comed_hourly_pricing.py
homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/cpuspeed.py
homeassistant/components/sensor/crimereports.py
homeassistant/components/sensor/cups.py homeassistant/components/sensor/cups.py
homeassistant/components/sensor/currencylayer.py homeassistant/components/sensor/currencylayer.py
homeassistant/components/sensor/darksky.py homeassistant/components/sensor/darksky.py
@ -332,6 +336,7 @@ omit =
homeassistant/components/sensor/dovado.py homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/ebox.py homeassistant/components/sensor/ebox.py
homeassistant/components/sensor/eddystone_temperature.py
homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/emoncms.py homeassistant/components/sensor/emoncms.py
homeassistant/components/sensor/fastdotcom.py homeassistant/components/sensor/fastdotcom.py
@ -357,9 +362,11 @@ omit =
homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/linux_battery.py
homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/lyft.py homeassistant/components/sensor/lyft.py
homeassistant/components/sensor/metoffice.py
homeassistant/components/sensor/miflora.py homeassistant/components/sensor/miflora.py
homeassistant/components/sensor/modem_callerid.py homeassistant/components/sensor/modem_callerid.py
homeassistant/components/sensor/mqtt_room.py homeassistant/components/sensor/mqtt_room.py
homeassistant/components/sensor/mvglive.py
homeassistant/components/sensor/netdata.py homeassistant/components/sensor/netdata.py
homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/nut.py homeassistant/components/sensor/nut.py
@ -431,12 +438,12 @@ omit =
homeassistant/components/tts/picotts.py homeassistant/components/tts/picotts.py
homeassistant/components/upnp.py homeassistant/components/upnp.py
homeassistant/components/weather/bom.py homeassistant/components/weather/bom.py
homeassistant/components/weather/metoffice.py
homeassistant/components/weather/openweathermap.py homeassistant/components/weather/openweathermap.py
homeassistant/components/weather/zamg.py homeassistant/components/weather/zamg.py
homeassistant/components/zeroconf.py homeassistant/components/zeroconf.py
homeassistant/components/zwave/__init__.py homeassistant/components/zwave/__init__.py
homeassistant/components/zwave/util.py homeassistant/components/zwave/util.py
homeassistant/components/zwave/workaround.py
[report] [report]

11
.gitignore vendored
View File

@ -1,15 +1,4 @@
config/* config/*
!config/home-assistant.conf.default
# There is not a better solution afaik..
!config/custom_components
config/custom_components/*
!config/custom_components/example.py
!config/custom_components/hello_world.py
!config/custom_components/mqtt_example.py
!config/panels
config/panels/*
!config/panels/react.html
tests/testing_config/deps tests/testing_config/deps
tests/testing_config/home-assistant.log tests/testing_config/home-assistant.log

View File

@ -1,9 +1,7 @@
Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/home-assistant/home-assistant| |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs| Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/home-assistant/home-assistant| |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs|
============================================================================================================================================================================================== ==============================================================================================================================================================================================
Home Assistant is a home automation platform running on Python 3. The Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
goal of Home Assistant is to be able to track and control all devices at
home and offer a platform for automating control.
To get started: To get started:
@ -12,83 +10,22 @@ To get started:
python3 -m pip install homeassistant python3 -m pip install homeassistant
hass --open-ui hass --open-ui
Check out `the website <https://home-assistant.io>`__ for `a Check out `home-assistant.io <https://home-assistant.io>`__ for `a
demo <https://home-assistant.io/demo/>`__, installation instructions, demo <https://home-assistant.io/demo/>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
tutorials and documentation. `tutorials <https://home-assistant.io/getting-started/automation-2/>`__ and `documentation <https://home-assistant.io/docs/>`__.
|screenshot-states| |screenshot-states|
Examples of devices Home Assistant can interface with: Featured integrations
---------------------
- Monitoring connected devices to a wireless router: |screenshot-components|
`OpenWrt <https://openwrt.org/>`__,
`Tomato <http://www.polarcloud.com/tomato>`__,
`Netgear <http://netgear.com>`__,
`DD-WRT <http://www.dd-wrt.com/site/index>`__,
`TPLink <http://www.tp-link.us/>`__,
`ASUSWRT <http://event.asus.com/2013/nw/ASUSWRT/>`__,
`Xiaomi <http://miwifi.com/>`__ and any SNMP
capable Linksys WAP/WRT
- `Philips Hue <http://meethue.com>`__ lights,
`WeMo <http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/>`__
switches, `Edimax <http://www.edimax.com/>`__ switches,
`Efergy <https://efergy.com>`__ energy monitoring, and
`Tellstick <http://www.telldus.se/products/tellstick>`__ devices and
sensors
- `Google
Chromecasts <http://www.google.com/intl/en/chrome/devices/chromecast>`__,
`Music Player Daemon <http://www.musicpd.org/>`__, `Logitech
Squeezebox <https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29>`__,
`Plex <https://plex.tv/>`__, `Kodi (XBMC) <http://kodi.tv/>`__,
iTunes (by way of
`itunes-api <https://github.com/maddox/itunes-api>`__), and Amazon
Fire TV (by way of
`python-firetv <https://github.com/happyleavesaoc/python-firetv>`__)
- Support for
`ISY994 <https://www.universal-devices.com/residential/isy994i-series/>`__
(Insteon and X10 devices), `Z-Wave <http://www.z-wave.com/>`__, `Nest
Thermostats <https://nest.com/>`__,
`RFXtrx <http://www.rfxcom.com/>`__,
`Arduino <https://www.arduino.cc/>`__, `Raspberry
Pi <https://www.raspberrypi.org/>`__, and
`Modbus <http://www.modbus.org/>`__
- Interaction with `IFTTT <https://ifttt.com/>`__
- Integrate data from the `Bitcoin <https://bitcoin.org>`__ network,
meteorological data from
`OpenWeatherMap <http://openweathermap.org/>`__ and
`Forecast.io <https://forecast.io/>`__,
`Transmission <http://www.transmissionbt.com/>`__, or
`SABnzbd <http://sabnzbd.org>`__.
- `See full list of supported
devices <https://home-assistant.io/components/>`__
Build home automation on top of your devices: The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture <https://home-assistant.io/developers/architecture/>`__ and the `section on creating your own
- Keep a precise history of every change to the state of your house
- Turn on the lights when people get home after sunset
- Turn on lights slowly during sunset to compensate for less light
- Turn off all lights and devices when everybody leaves the house
- Offers a `REST API <https://home-assistant.io/developers/rest_api/>`__
and can interface with MQTT for easy integration with other projects
like `OwnTracks <http://owntracks.org/>`__
- Allow sending notifications using
`Instapush <https://instapush.im>`__, `Notify My Android
(NMA) <http://www.notifymyandroid.com/>`__,
`PushBullet <https://www.pushbullet.com/>`__,
`PushOver <https://pushover.net/>`__,
`Slack <https://slack.com/>`__,
`Telegram <https://telegram.org/>`__, `Join <http://joaoapps.com/join/>`__, and `Jabber
(XMPP) <http://xmpp.org>`__
The system is built using a modular approach so support for other devices or actions can
be implemented easily. See also the `section on
architecture <https://home-assistant.io/developers/architecture/>`__
and the `section on creating your own
components <https://home-assistant.io/developers/creating_components/>`__. components <https://home-assistant.io/developers/creating_components/>`__.
If you run into issues while using Home Assistant or during development If you run into issues while using Home Assistant or during development
of a component, check the `Home Assistant help of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
section <https://home-assistant.io/help/>`__ of our website for further help and information.
.. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=master .. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=master
:target: https://travis-ci.org/home-assistant/home-assistant :target: https://travis-ci.org/home-assistant/home-assistant
@ -100,3 +37,5 @@ section <https://home-assistant.io/help/>`__ of our website for further help and
:target: https://gitter.im/home-assistant/home-assistant/devs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge :target: https://gitter.im/home-assistant/home-assistant/devs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png .. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
:target: https://home-assistant.io/demo/ :target: https://home-assistant.io/demo/
.. |screenshot-components| image:: https://raw.github.com/home-assistant/home-assistant/dev/docs/screenshot-components.png
:target: https://home-assistant.io/components/

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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>

BIN
docs/screenshot-components.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -20,6 +20,17 @@ from homeassistant.const import (
from homeassistant.util.async import run_callback_threadsafe from homeassistant.util.async import run_callback_threadsafe
def attempt_use_uvloop():
"""Attempt to use uvloop."""
import asyncio
try:
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
def monkey_patch_asyncio(): def monkey_patch_asyncio():
"""Replace weakref.WeakSet to address Python 3 bug. """Replace weakref.WeakSet to address Python 3 bug.
@ -311,8 +322,7 @@ def setup_and_run_hass(config_dir: str,
EVENT_HOMEASSISTANT_START, open_browser EVENT_HOMEASSISTANT_START, open_browser
) )
hass.start() return hass.start()
return hass.exit_code
def try_to_restart() -> None: def try_to_restart() -> None:
@ -359,11 +369,13 @@ def try_to_restart() -> None:
def main() -> int: def main() -> int:
"""Start Home Assistant.""" """Start Home Assistant."""
validate_python()
attempt_use_uvloop()
if sys.version_info[:3] < (3, 5, 3): if sys.version_info[:3] < (3, 5, 3):
monkey_patch_asyncio() monkey_patch_asyncio()
validate_python()
args = get_arguments() args = get_arguments()
if args.script is not None: if args.script is not None:

View File

@ -74,8 +74,6 @@ def async_from_config_dict(config: Dict[str, Any],
This method is a coroutine. This method is a coroutine.
""" """
start = time() start = time()
hass.async_track_tasks()
core_config = config.get(core.DOMAIN, {}) core_config = config.get(core.DOMAIN, {})
try: try:
@ -140,10 +138,10 @@ def async_from_config_dict(config: Dict[str, Any],
continue continue
hass.async_add_job(async_setup_component(hass, component, config)) hass.async_add_job(async_setup_component(hass, component, config))
yield from hass.async_stop_track_tasks() yield from hass.async_block_till_done()
stop = time() stop = time()
_LOGGER.info('Home Assistant initialized in %ss', round(stop-start, 2)) _LOGGER.info('Home Assistant initialized in %.2fs', stop-start)
async_register_signal_handling(hass) async_register_signal_handling(hass)
return hass return hass

View File

@ -1,13 +1,13 @@
""" """
Interfaces with Alarm.com alarm control panels. Interfaces with Alarm.com alarm control panels.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.alarmdotcom/ https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
""" """
import logging import logging
import asyncio
import voluptuous as vol import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
@ -15,10 +15,9 @@ from homeassistant.const import (
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN, CONF_CODE, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN, CONF_CODE,
CONF_NAME) CONF_NAME)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
REQUIREMENTS = ['https://github.com/Xorso/pyalarmdotcom' REQUIREMENTS = ['pyalarmdotcom==0.2.9']
'/archive/0.1.1.zip'
'#pyalarmdotcom==0.1.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,14 +31,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
def setup_platform(hass, config, add_devices, discovery_info=None): @asyncio.coroutine
"""Setup an Alarm.com control panel.""" def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a Alarm.com control panel."""
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
code = config.get(CONF_CODE) code = config.get(CONF_CODE)
username = config.get(CONF_USERNAME) username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD) password = config.get(CONF_PASSWORD)
add_devices([AlarmDotCom(hass, name, code, username, password)], True) alarmdotcom = AlarmDotCom(hass, name, code, username, password)
yield from alarmdotcom.async_login()
async_add_devices([alarmdotcom])
class AlarmDotCom(alarm.AlarmControlPanel): class AlarmDotCom(alarm.AlarmControlPanel):
@ -47,18 +49,30 @@ class AlarmDotCom(alarm.AlarmControlPanel):
def __init__(self, hass, name, code, username, password): def __init__(self, hass, name, code, username, password):
"""Initialize the Alarm.com status.""" """Initialize the Alarm.com status."""
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom from pyalarmdotcom import Alarmdotcom
self._alarm = Alarmdotcom(username, password, timeout=10) _LOGGER.debug('Setting up Alarm.com...')
self._hass = hass self._hass = hass
self._name = name self._name = name
self._code = str(code) if code else None self._code = str(code) if code else None
self._username = username self._username = username
self._password = password self._password = password
self._websession = async_get_clientsession(self._hass)
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._alarm = Alarmdotcom(username,
password,
self._websession,
hass.loop)
def update(self): @asyncio.coroutine
def async_login(self):
"""Login to Alarm.com."""
yield from self._alarm.async_login()
@asyncio.coroutine
def async_update(self):
"""Fetch the latest state.""" """Fetch the latest state."""
self._state = self._alarm.state yield from self._alarm.async_update()
return self._alarm.state
@property @property
def name(self): def name(self):
@ -73,45 +87,36 @@ class AlarmDotCom(alarm.AlarmControlPanel):
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
if self._state == 'Disarmed': if self._alarm.state.lower() == 'disarmed':
return STATE_ALARM_DISARMED return STATE_ALARM_DISARMED
elif self._state == 'Armed Stay': elif self._alarm.state.lower() == 'armed stay':
return STATE_ALARM_ARMED_HOME return STATE_ALARM_ARMED_HOME
elif self._state == 'Armed Away': elif self._alarm.state.lower() == 'armed away':
return STATE_ALARM_ARMED_AWAY return STATE_ALARM_ARMED_AWAY
else: else:
return STATE_UNKNOWN return STATE_UNKNOWN
def alarm_disarm(self, code=None): @asyncio.coroutine
def async_alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
if not self._validate_code(code, 'disarming home'): if self._validate_code(code):
return yield from self._alarm.async_alarm_disarm()
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
# Open another session to alarm.com to fire off the command
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
_alarm.disarm()
def alarm_arm_home(self, code=None): @asyncio.coroutine
"""Send arm home command.""" def async_alarm_arm_home(self, code=None):
if not self._validate_code(code, 'arming home'): """Send arm hom command."""
return if self._validate_code(code):
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom yield from self._alarm.async_alarm_arm_home()
# Open another session to alarm.com to fire off the command
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
_alarm.arm_stay()
def alarm_arm_away(self, code=None): @asyncio.coroutine
def async_alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
if not self._validate_code(code, 'arming home'): if self._validate_code(code):
return yield from self._alarm.async_alarm_arm_away()
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
# Open another session to alarm.com to fire off the command
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
_alarm.arm_away()
def _validate_code(self, code, state): def _validate_code(self, code):
"""Validate given code.""" """Validate given code."""
check = self._code is None or code == self._code check = self._code is None or code == self._code
if not check: if not check:
_LOGGER.warning('Wrong code entered for %s', state) _LOGGER.warning('Wrong code entered.')
return check return check

View File

@ -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()

View File

@ -26,17 +26,24 @@ from homeassistant.util.dt import utcnow
from homeassistant.components.camera.mjpeg import ( from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL) CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
DOMAIN = 'android_ip_webcam' REQUIREMENTS = ['pydroid-ipcam==0.8']
REQUIREMENTS = ["pydroid-ipcam==0.6"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10)
DATA_IP_WEBCAM = 'android_ip_webcam'
ATTR_AUD_CONNS = 'Audio Connections'
ATTR_HOST = 'host' ATTR_HOST = 'host'
ATTR_VID_CONNS = 'Video Connections' ATTR_VID_CONNS = 'Video Connections'
ATTR_AUD_CONNS = 'Audio Connections'
CONF_MOTION_SENSOR = 'motion_sensor'
DATA_IP_WEBCAM = 'android_ip_webcam'
DEFAULT_NAME = 'IP Webcam'
DEFAULT_PORT = 8080
DEFAULT_TIMEOUT = 10
DOMAIN = 'android_ip_webcam'
SCAN_INTERVAL = timedelta(seconds=10)
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
KEY_MAP = { KEY_MAP = {
'audio_connections': 'Audio Connections', 'audio_connections': 'Audio Connections',
@ -123,15 +130,6 @@ SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
'battery_voltage', 'light', 'motion', 'pressure', 'proximity', 'battery_voltage', 'light', 'motion', 'pressure', 'proximity',
'sound', 'video_connections'] 'sound', 'video_connections']
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
CONF_MOTION_SENSOR = 'motion_sensor'
DEFAULT_NAME = 'IP Webcam'
DEFAULT_PORT = 8080
DEFAULT_TIMEOUT = 10
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@ -153,7 +151,7 @@ CONFIG_SCHEMA = vol.Schema({
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Setup the IP Webcam component.""" """Set up the IP Webcam component."""
from pydroid_ipcam import PyDroidIPCam from pydroid_ipcam import PyDroidIPCam
webcams = hass.data[DATA_IP_WEBCAM] = {} webcams = hass.data[DATA_IP_WEBCAM] = {}
@ -161,7 +159,7 @@ def async_setup(hass, config):
@asyncio.coroutine @asyncio.coroutine
def async_setup_ipcamera(cam_config): def async_setup_ipcamera(cam_config):
"""Setup a ip camera.""" """Set up an IP camera."""
host = cam_config[CONF_HOST] host = cam_config[CONF_HOST]
username = cam_config.get(CONF_USERNAME) username = cam_config.get(CONF_USERNAME)
password = cam_config.get(CONF_PASSWORD) password = cam_config.get(CONF_PASSWORD)
@ -171,7 +169,7 @@ def async_setup(hass, config):
sensors = cam_config[CONF_SENSORS] sensors = cam_config[CONF_SENSORS]
motion = cam_config[CONF_MOTION_SENSOR] motion = cam_config[CONF_MOTION_SENSOR]
# init ip webcam # Init ip webcam
cam = PyDroidIPCam( cam = PyDroidIPCam(
hass.loop, websession, host, cam_config[CONF_PORT], hass.loop, websession, host, cam_config[CONF_PORT],
username=username, password=password, username=username, password=password,
@ -192,7 +190,7 @@ def async_setup(hass, config):
@asyncio.coroutine @asyncio.coroutine
def async_update_data(now): def async_update_data(now):
"""Update data from ipcam in SCAN_INTERVAL.""" """Update data from IP camera in SCAN_INTERVAL."""
yield from cam.update() yield from cam.update()
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host) async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
@ -201,7 +199,7 @@ def async_setup(hass, config):
yield from async_update_data(None) yield from async_update_data(None)
# load platforms # Load platforms
webcams[host] = cam webcams[host] = cam
mjpeg_camera = { mjpeg_camera = {

View File

@ -50,9 +50,11 @@ def setup(hass, config):
hass.http.register_view(APIDomainServicesView) hass.http.register_view(APIDomainServicesView)
hass.http.register_view(APIEventForwardingView) hass.http.register_view(APIEventForwardingView)
hass.http.register_view(APIComponentsView) hass.http.register_view(APIComponentsView)
hass.http.register_view(APIErrorLogView)
hass.http.register_view(APITemplateView) hass.http.register_view(APITemplateView)
hass.http.register_static_path(
URL_API_ERROR_LOG, hass.config.path(ERROR_LOG_FILENAME), False)
return True return True
@ -402,20 +404,6 @@ class APIComponentsView(HomeAssistantView):
return self.json(request.app['hass'].config.components) return self.json(request.app['hass'].config.components)
class APIErrorLogView(HomeAssistantView):
"""View to handle ErrorLog requests."""
url = URL_API_ERROR_LOG
name = "api:error-log"
@asyncio.coroutine
def get(self, request):
"""Serve error log."""
resp = yield from self.file(
request, request.app['hass'].config.path(ERROR_LOG_FILENAME))
return resp
class APITemplateView(HomeAssistantView): class APITemplateView(HomeAssistantView):
"""View to handle requests.""" """View to handle requests."""

View File

@ -12,10 +12,11 @@ import os
import voluptuous as vol import voluptuous as vol
from homeassistant.setup import async_prepare_setup_platform from homeassistant.setup import async_prepare_setup_platform
from homeassistant.core import CoreState
from homeassistant import config as conf_util from homeassistant import config as conf_util
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
SERVICE_TOGGLE, SERVICE_RELOAD) SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START)
from homeassistant.components import logbook from homeassistant.components import logbook
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import extract_domain_configs, script, condition from homeassistant.helpers import extract_domain_configs, script, condition
@ -81,8 +82,7 @@ _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
PLATFORM_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = vol.Schema({
CONF_ALIAS: cv.string, CONF_ALIAS: cv.string,
vol.Optional(CONF_INITIAL_STATE, vol.Optional(CONF_INITIAL_STATE): cv.boolean,
default=DEFAULT_INITIAL_STATE): cv.boolean,
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean, vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
@ -101,15 +101,13 @@ TRIGGER_SERVICE_SCHEMA = vol.Schema({
RELOAD_SERVICE_SCHEMA = vol.Schema({}) RELOAD_SERVICE_SCHEMA = vol.Schema({})
def is_on(hass, entity_id=None): def is_on(hass, entity_id):
""" """
Return true if specified automation entity_id is on. Return true if specified automation entity_id is on.
Check all automation if no entity_id specified. Async friendly.
""" """
entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN) return hass.states.is_state(entity_id, STATE_ON)
return any(hass.states.is_state(entity_id, STATE_ON)
for entity_id in entity_ids)
def turn_on(hass, entity_id=None): def turn_on(hass, entity_id=None):
@ -231,7 +229,6 @@ class AutomationEntity(ToggleEntity):
self._async_detach_triggers = None self._async_detach_triggers = None
self._cond_func = cond_func self._cond_func = cond_func
self._async_action = async_action self._async_action = async_action
self._enabled = False
self._last_triggered = None self._last_triggered = None
self._hidden = hidden self._hidden = hidden
self._initial_state = initial_state self._initial_state = initial_state
@ -261,38 +258,54 @@ class AutomationEntity(ToggleEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return True if entity is on.""" """Return True if entity is on."""
return self._enabled return self._async_detach_triggers is not None
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self) -> None: def async_added_to_hass(self) -> None:
"""Startup with initial state or previous state.""" """Startup with initial state or previous state."""
state = yield from async_get_last_state(self.hass, self.entity_id) enable_automation = DEFAULT_INITIAL_STATE
if state is None:
if self._initial_state: if self._initial_state is not None:
yield from self.async_enable() enable_automation = self._initial_state
else: else:
state = yield from async_get_last_state(self.hass, self.entity_id)
if state:
enable_automation = state.state == STATE_ON
self._last_triggered = state.attributes.get('last_triggered') self._last_triggered = state.attributes.get('last_triggered')
if state.state == STATE_ON:
if not enable_automation:
return
# HomeAssistant is starting up
elif self.hass.state == CoreState.not_running:
@asyncio.coroutine
def async_enable_automation(event):
"""Start automation on startup."""
yield from self.async_enable()
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, async_enable_automation)
# HomeAssistant is running
else:
yield from self.async_enable() yield from self.async_enable()
@asyncio.coroutine @asyncio.coroutine
def async_turn_on(self, **kwargs) -> None: def async_turn_on(self, **kwargs) -> None:
"""Turn the entity on and update the state.""" """Turn the entity on and update the state."""
if self._enabled: if self.is_on:
return return
yield from self.async_enable() yield from self.async_enable()
yield from self.async_update_ha_state()
@asyncio.coroutine @asyncio.coroutine
def async_turn_off(self, **kwargs) -> None: def async_turn_off(self, **kwargs) -> None:
"""Turn the entity off.""" """Turn the entity off."""
if not self._enabled: if not self.is_on:
return return
self._async_detach_triggers() self._async_detach_triggers()
self._async_detach_triggers = None self._async_detach_triggers = None
self._enabled = False
yield from self.async_update_ha_state() yield from self.async_update_ha_state()
@asyncio.coroutine @asyncio.coroutine
@ -318,12 +331,12 @@ class AutomationEntity(ToggleEntity):
This method is a coroutine. This method is a coroutine.
""" """
if self._enabled: if self.is_on:
return return
self._async_detach_triggers = yield from self._async_attach_triggers( self._async_detach_triggers = yield from self._async_attach_triggers(
self.async_trigger) self.async_trigger)
self._enabled = True yield from self.async_update_ha_state()
@asyncio.coroutine @asyncio.coroutine
@ -342,7 +355,7 @@ def _async_process_config(hass, config, component):
list_no) list_no)
hidden = config_block[CONF_HIDE_ENTITY] hidden = config_block[CONF_HIDE_ENTITY]
initial_state = config_block[CONF_INITIAL_STATE] initial_state = config_block.get(CONF_INITIAL_STATE)
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
name) name)

View File

@ -2,15 +2,15 @@
Offer event listening automation rules. Offer event listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#event-trigger at https://home-assistant.io/docs/automation/trigger/#event-trigger
""" """
import asyncio import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback, CoreState
from homeassistant.const import CONF_PLATFORM from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_START
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
CONF_EVENT_TYPE = "event_type" CONF_EVENT_TYPE = "event_type"
@ -31,6 +31,19 @@ def async_trigger(hass, config, action):
event_type = config.get(CONF_EVENT_TYPE) event_type = config.get(CONF_EVENT_TYPE)
event_data = config.get(CONF_EVENT_DATA) event_data = config.get(CONF_EVENT_DATA)
if (event_type == EVENT_HOMEASSISTANT_START and
hass.state == CoreState.starting):
_LOGGER.warning('Deprecation: Automations should not listen to event '
"'homeassistant_start'. Use platform 'homeassistant' "
'instead. Feature will be removed in 0.45')
hass.async_run_job(action, {
'trigger': {
'platform': 'event',
'event': None,
},
})
return lambda: None
@callback @callback
def handle_event(event): def handle_event(event):
"""Listen for events and calls the action when data matches.""" """Listen for events and calls the action when data matches."""

View File

@ -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

View File

@ -70,7 +70,7 @@ def async_trigger(hass, config, action):
nonlocal held_less_than, held_more_than nonlocal held_less_than, held_more_than
pressed_time = dt_util.utcnow() pressed_time = dt_util.utcnow()
if held_more_than is None and held_less_than is None: if held_more_than is None and held_less_than is None:
call_action() hass.add_job(call_action)
if held_more_than is not None and held_less_than is None: if held_more_than is not None and held_less_than is None:
cancel_pressed_more_than = track_point_in_utc_time( cancel_pressed_more_than = track_point_in_utc_time(
hass, hass,
@ -88,7 +88,7 @@ def async_trigger(hass, config, action):
held_time = dt_util.utcnow() - pressed_time held_time = dt_util.utcnow() - pressed_time
if held_less_than is not None and held_time < held_less_than: if held_less_than is not None and held_time < held_less_than:
if held_more_than is None or held_time > held_more_than: if held_more_than is None or held_time > held_more_than:
call_action() hass.add_job(call_action)
hass.data['litejet_system'].on_switch_pressed(number, pressed) hass.data['litejet_system'].on_switch_pressed(number, pressed)
hass.data['litejet_system'].on_switch_released(number, released) hass.data['litejet_system'].on_switch_released(number, released)

View File

@ -2,7 +2,7 @@
Offer MQTT listening automation rules. Offer MQTT listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#mqtt-trigger at https://home-assistant.io/docs/automation/trigger/#mqtt-trigger
""" """
import asyncio import asyncio
import json import json

View File

@ -2,7 +2,7 @@
Offer numeric state listening automation rules. Offer numeric state listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#numeric-state-trigger at https://home-assistant.io/docs/automation/trigger/#numeric-state-trigger
""" """
import asyncio import asyncio
import logging import logging

View File

@ -2,7 +2,7 @@
Offer state listening automation rules. Offer state listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#state-trigger at https://home-assistant.io/docs/automation/trigger/#state-trigger
""" """
import asyncio import asyncio
import voluptuous as vol import voluptuous as vol

View File

@ -2,7 +2,7 @@
Offer sun based automation rules. Offer sun based automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#sun-trigger at https://home-assistant.io/docs/automation/trigger/#sun-trigger
""" """
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta

View File

@ -2,7 +2,7 @@
Offer template automation rules. Offer template automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#template-trigger at https://home-assistant.io/docs/automation/trigger/#template-trigger
""" """
import asyncio import asyncio
import logging import logging

View File

@ -2,7 +2,7 @@
Offer time listening automation rules. Offer time listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#time-trigger at https://home-assistant.io/docs/automation/trigger/#time-trigger
""" """
import asyncio import asyncio
import logging import logging

View File

@ -2,7 +2,7 @@
Offer zone automation rules. Offer zone automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#zone-trigger at https://home-assistant.io/docs/automation/trigger/#zone-trigger
""" """
import asyncio import asyncio
import voluptuous as vol import voluptuous as vol

View File

@ -11,7 +11,7 @@ DEPENDENCIES = ['blink']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the blink binary sensors.""" """Set up the blink binary sensors."""
if discovery_info is None: if discovery_info is None:
return return

View File

@ -18,7 +18,7 @@ from homeassistant.const import (
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE) ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
REQUIREMENTS = ['pyhik==0.1.0'] REQUIREMENTS = ['pyhik==0.1.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_IGNORED = 'ignored' CONF_IGNORED = 'ignored'
@ -33,7 +33,6 @@ ATTR_DELAY = 'delay'
DEVICE_CLASS_MAP = { DEVICE_CLASS_MAP = {
'Motion': 'motion', 'Motion': 'motion',
'Line Crossing': 'motion', 'Line Crossing': 'motion',
'IO Trigger': None,
'Field Detection': 'motion', 'Field Detection': 'motion',
'Video Loss': None, 'Video Loss': None,
'Tamper Detection': 'motion', 'Tamper Detection': 'motion',
@ -47,6 +46,7 @@ DEVICE_CLASS_MAP = {
'Bad Video': None, 'Bad Video': None,
'PIR Alarm': 'motion', 'PIR Alarm': 'motion',
'Face Detection': 'motion', 'Face Detection': 'motion',
'Scene Change Detection': 'motion',
} }
CUSTOMIZE_SCHEMA = vol.Schema({ CUSTOMIZE_SCHEMA = vol.Schema({
@ -91,8 +91,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
entities = [] entities = []
for sensor in data.sensors: for sensor, channel_list in data.sensors.items():
for channel in channel_list:
# Build sensor name, then parse customize config. # Build sensor name, then parse customize config.
if data.type == 'NVR':
sensor_name = '{}_{}'.format(
sensor.replace(' ', '_'), channel[1])
else:
sensor_name = sensor.replace(' ', '_') sensor_name = sensor.replace(' ', '_')
custom = customize.get(sensor_name.lower(), {}) custom = customize.get(sensor_name.lower(), {})
@ -102,13 +107,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.debug('Entity: %s - %s, Options - Ignore: %s, Delay: %s', _LOGGER.debug('Entity: %s - %s, Options - Ignore: %s, Delay: %s',
data.name, sensor_name, ignore, delay) data.name, sensor_name, ignore, delay)
if not ignore: if not ignore:
entities.append(HikvisionBinarySensor(hass, sensor, data, delay)) entities.append(HikvisionBinarySensor(
hass, sensor, channel[1], data, delay))
add_entities(entities) add_entities(entities)
class HikvisionData(object): class HikvisionData(object):
"""Hikvision camera event stream object.""" """Hikvision device event stream object."""
def __init__(self, hass, url, port, name, username, password): def __init__(self, hass, url, port, name, username, password):
"""Initialize the data oject.""" """Initialize the data oject."""
@ -144,25 +150,40 @@ class HikvisionData(object):
@property @property
def cam_id(self): def cam_id(self):
"""Return camera id.""" """Return device id."""
return self.camdata.get_id return self.camdata.get_id
@property @property
def name(self): def name(self):
"""Return camera name.""" """Return device name."""
return self._name return self._name
@property
def type(self):
"""Return device type."""
return self.camdata.get_type
def get_attributes(self, sensor, channel):
"""Return attribute list for sensor/channel."""
return self.camdata.fetch_attributes(sensor, channel)
class HikvisionBinarySensor(BinarySensorDevice): class HikvisionBinarySensor(BinarySensorDevice):
"""Representation of a Hikvision binary sensor.""" """Representation of a Hikvision binary sensor."""
def __init__(self, hass, sensor, cam, delay): def __init__(self, hass, sensor, channel, cam, delay):
"""Initialize the binary_sensor.""" """Initialize the binary_sensor."""
self._hass = hass self._hass = hass
self._cam = cam self._cam = cam
self._name = self._cam.name + ' ' + sensor
self._id = self._cam.cam_id + '.' + sensor
self._sensor = sensor self._sensor = sensor
self._channel = channel
if self._cam.type == 'NVR':
self._name = '{} {} {}'.format(self._cam.name, sensor, channel)
else:
self._name = '{} {}'.format(self._cam.name, sensor)
self._id = '{}.{}.{}'.format(self._cam.cam_id, sensor, channel)
if delay is None: if delay is None:
self._delay = 0 self._delay = 0
@ -176,11 +197,11 @@ class HikvisionBinarySensor(BinarySensorDevice):
def _sensor_state(self): def _sensor_state(self):
"""Extract sensor state.""" """Extract sensor state."""
return self._cam.sensors[self._sensor][0] return self._cam.get_attributes(self._sensor, self._channel)[0]
def _sensor_last_update(self): def _sensor_last_update(self):
"""Extract sensor last update time.""" """Extract sensor last update time."""
return self._cam.sensors[self._sensor][3] return self._cam.get_attributes(self._sensor, self._channel)[3]
@property @property
def name(self): def name(self):

View File

@ -32,7 +32,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
InsteonPLMBinarySensorDevice(hass, plm, address, name) InsteonPLMBinarySensorDevice(hass, plm, address, name)
) )
hass.async_add_job(async_add_devices(device_list)) async_add_devices(device_list)
class InsteonPLMBinarySensorDevice(BinarySensorDevice): class InsteonPLMBinarySensorDevice(BinarySensorDevice):

View File

@ -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

View File

@ -1,4 +1,9 @@
"""Sensor to indicate whether the current day is a workday.""" """
Sensor to indicate whether the current day is a workday.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.workday/
"""
import asyncio import asyncio
import logging import logging
import datetime import datetime
@ -6,8 +11,8 @@ import datetime
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN, from homeassistant.const import (
CONF_NAME, WEEKDAYS) STATE_ON, STATE_OFF, STATE_UNKNOWN, CONF_NAME, WEEKDAYS)
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -48,29 +53,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Workday sensor.""" """Set up the Workday sensor."""
import holidays import holidays
# Get the Sensor name from the config
sensor_name = config.get(CONF_NAME) sensor_name = config.get(CONF_NAME)
# Get the country code from the config
country = config.get(CONF_COUNTRY) country = config.get(CONF_COUNTRY)
# Get the province from the config
province = config.get(CONF_PROVINCE) province = config.get(CONF_PROVINCE)
# Get the list of workdays from the config
workdays = config.get(CONF_WORKDAYS) workdays = config.get(CONF_WORKDAYS)
# Get the list of excludes from the config
excludes = config.get(CONF_EXCLUDES) excludes = config.get(CONF_EXCLUDES)
# Instantiate the holidays module for the current year
year = datetime.datetime.now().year year = datetime.datetime.now().year
obj_holidays = getattr(holidays, country)(years=year) obj_holidays = getattr(holidays, country)(years=year)
# Also apply the provience, if available for the configured country
if province: if province:
if province not in obj_holidays.PROVINCES: if province not in obj_holidays.PROVINCES:
_LOGGER.error('There is no province/state %s in country %s', _LOGGER.error('There is no province/state %s in country %s',
@ -81,14 +75,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
obj_holidays = getattr(holidays, country)(prov=province, obj_holidays = getattr(holidays, country)(prov=province,
years=year) years=year)
# Output found public holidays via the debug channel
_LOGGER.debug("Found the following holidays for your configuration:") _LOGGER.debug("Found the following holidays for your configuration:")
for date, name in sorted(obj_holidays.items()): for date, name in sorted(obj_holidays.items()):
_LOGGER.debug("%s %s", date, name) _LOGGER.debug("%s %s", date, name)
# Add ourselves as device add_devices([IsWorkdaySensor(
add_devices([IsWorkdaySensor(obj_holidays, workdays, obj_holidays, workdays, excludes, sensor_name)], True)
excludes, sensor_name)], True)
def day_to_string(day): def day_to_string(day):
@ -122,7 +114,6 @@ class IsWorkdaySensor(Entity):
def is_include(self, day, now): def is_include(self, day, now):
"""Check if given day is in the includes list.""" """Check if given day is in the includes list."""
# Check includes
if day in self._workdays: if day in self._workdays:
return True return True
elif 'holiday' in self._workdays and now in self._obj_holidays: elif 'holiday' in self._workdays and now in self._obj_holidays:
@ -132,7 +123,6 @@ class IsWorkdaySensor(Entity):
def is_exclude(self, day, now): def is_exclude(self, day, now):
"""Check if given day is in the excludes list.""" """Check if given day is in the excludes list."""
# Check excludes
if day in self._excludes: if day in self._excludes:
return True return True
elif 'holiday' in self._excludes and now in self._obj_holidays: elif 'holiday' in self._excludes and now in self._obj_holidays:

View File

@ -5,17 +5,19 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/blink/ https://home-assistant.io/components/blink/
""" """
import logging import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import (CONF_USERNAME, from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, CONF_PASSWORD, ATTR_FRIENDLY_NAME, ATTR_ARMED)
ATTR_FRIENDLY_NAME,
ATTR_ARMED)
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
REQUIREMENTS = ['blinkpy==0.5.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'blink' DOMAIN = 'blink'
REQUIREMENTS = ['blinkpy==0.5.2']
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -50,7 +52,7 @@ class BlinkSystem(object):
def setup(hass, config): def setup(hass, config):
"""Setup Blink System.""" """Set up Blink System."""
hass.data[DOMAIN] = BlinkSystem(config) hass.data[DOMAIN] = BlinkSystem(config)
discovery.load_platform(hass, 'camera', DOMAIN, {}, config) discovery.load_platform(hass, 'camera', DOMAIN, {}, config)
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
@ -77,11 +79,11 @@ def setup(hass, config):
hass.data[DOMAIN].blink.arm = value hass.data[DOMAIN].blink.arm = value
hass.data[DOMAIN].blink.refresh() hass.data[DOMAIN].blink.refresh()
hass.services.register(DOMAIN, 'snap_picture', snap_picture, hass.services.register(
schema=SNAP_PICTURE_SCHEMA) DOMAIN, 'snap_picture', snap_picture, schema=SNAP_PICTURE_SCHEMA)
hass.services.register(DOMAIN, 'arm_camera', arm_camera, hass.services.register(
schema=ARM_CAMERA_SCHEMA) DOMAIN, 'arm_camera', arm_camera, schema=ARM_CAMERA_SCHEMA)
hass.services.register(DOMAIN, 'arm_system', arm_system, hass.services.register(
schema=ARM_SYSTEM_SCHEMA) DOMAIN, 'arm_system', arm_system, schema=ARM_SYSTEM_SCHEMA)
return True return True

View File

@ -7,6 +7,7 @@ https://home-assistant.io/components/camera/
""" """
import asyncio import asyncio
import collections import collections
from contextlib import suppress
from datetime import timedelta from datetime import timedelta
import logging import logging
import hashlib import hashlib
@ -58,7 +59,6 @@ def async_get_image(hass, entity_id, timeout=10):
state.attributes.get(ATTR_ENTITY_PICTURE) state.attributes.get(ATTR_ENTITY_PICTURE)
) )
response = None
try: try:
with async_timeout.timeout(timeout, loop=hass.loop): with async_timeout.timeout(timeout, loop=hass.loop):
response = yield from websession.get(url) response = yield from websession.get(url)
@ -70,13 +70,9 @@ def async_get_image(hass, entity_id, timeout=10):
image = yield from response.read() image = yield from response.read()
return image return image
except (asyncio.TimeoutError, aiohttp.errors.ClientError): except (asyncio.TimeoutError, aiohttp.ClientError):
raise HomeAssistantError("Can't connect to {0}".format(url)) raise HomeAssistantError("Can't connect to {0}".format(url))
finally:
if response is not None:
yield from response.release()
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
@ -172,7 +168,7 @@ class Camera(Entity):
if not img_bytes: if not img_bytes:
break break
if img_bytes is not None and img_bytes != last_image: if img_bytes and img_bytes != last_image:
write(img_bytes) write(img_bytes)
# Chrome seems to always ignore first picture, # Chrome seems to always ignore first picture,
@ -185,8 +181,8 @@ class Camera(Entity):
yield from asyncio.sleep(.5) yield from asyncio.sleep(.5)
except (asyncio.CancelledError, ConnectionResetError): except asyncio.CancelledError:
_LOGGER.debug("Close stream by frontend.") _LOGGER.debug("Stream closed by frontend.")
response = None response = None
finally: finally:
@ -268,16 +264,14 @@ class CameraImageView(CameraView):
@asyncio.coroutine @asyncio.coroutine
def handle(self, request, camera): def handle(self, request, camera):
"""Serve camera image.""" """Serve camera image."""
try: with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(10, loop=request.app['hass'].loop):
image = yield from camera.async_camera_image() image = yield from camera.async_camera_image()
if image is None: if image:
return web.Response(status=500)
return web.Response(body=image) return web.Response(body=image)
except asyncio.CancelledError: return web.Response(status=500)
_LOGGER.debug("Close stream by frontend.")
class CameraMjpegStream(CameraView): class CameraMjpegStream(CameraView):

View File

@ -16,9 +16,9 @@ from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT) CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import ( from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_aiohttp_proxy_stream) async_get_clientsession, async_aiohttp_proxy_web)
REQUIREMENTS = ['amcrest==1.1.4'] REQUIREMENTS = ['amcrest==1.1.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -125,7 +125,7 @@ class AmcrestCam(Camera):
stream_coro = websession.get( stream_coro = websession.get(
streaming_url, auth=self._token, timeout=TIMEOUT) streaming_url, auth=self._token, timeout=TIMEOUT)
yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro) yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
@property @property
def name(self): def name(self):

View File

@ -8,14 +8,14 @@ import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from aiohttp import web
from homeassistant.const import CONF_NAME
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import ( from homeassistant.components.ffmpeg import (
DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS) DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_stream)
DEPENDENCIES = ['ffmpeg'] DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -69,26 +69,10 @@ class FFmpegCamera(Camera):
yield from stream.open_camera( yield from stream.open_camera(
self._input, extra_cmd=self._extra_arguments) self._input, extra_cmd=self._extra_arguments)
response = web.StreamResponse() yield from async_aiohttp_proxy_stream(
response.content_type = 'multipart/x-mixed-replace;boundary=ffserver' self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
yield from response.prepare(request)
try:
while True:
data = yield from stream.read(102400)
if not data:
break
response.write(data)
except asyncio.CancelledError:
_LOGGER.debug("Close stream by frontend.")
response = None
finally:
yield from stream.close() yield from stream.close()
if response is not None:
yield from response.write_eof()
@property @property
def name(self): def name(self):

View File

@ -66,8 +66,12 @@ class FoscamCamera(Camera):
def camera_image(self): def camera_image(self):
"""Return a still image reponse from the camera.""" """Return a still image reponse from the camera."""
# Send the request to snap a picture and return raw jpg data # Send the request to snap a picture and return raw jpg data
# Handle exception if host is not reachable or url failed
try:
response = requests.get(self._snap_picture_url, timeout=10) response = requests.get(self._snap_picture_url, timeout=10)
except requests.exceptions.ConnectionError:
return None
else:
return response.content return response.content
@property @property

View File

@ -107,7 +107,6 @@ class GenericCamera(Camera):
None, fetch) None, fetch)
# async # async
else: else:
response = None
try: try:
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
with async_timeout.timeout(10, loop=self.hass.loop): with async_timeout.timeout(10, loop=self.hass.loop):
@ -117,14 +116,9 @@ class GenericCamera(Camera):
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.error('Timeout getting camera image') _LOGGER.error('Timeout getting camera image')
return self._last_image return self._last_image
except (aiohttp.errors.ClientError, except aiohttp.ClientError as err:
aiohttp.errors.DisconnectedError,
aiohttp.errors.HttpProcessingError) as err:
_LOGGER.error('Error getting new camera image: %s', err) _LOGGER.error('Error getting new camera image: %s', err)
return self._last_image return self._last_image
finally:
if response is not None:
yield from response.release()
self._last_url = url self._last_url = url
return self._last_image return self._last_image

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.helpers.aiohttp_client import ( from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_aiohttp_proxy_stream) async_get_clientsession, async_aiohttp_proxy_web)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -93,7 +93,6 @@ class MjpegCamera(Camera):
return image return image
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
response = None
try: try:
with async_timeout.timeout(10, loop=self.hass.loop): with async_timeout.timeout(10, loop=self.hass.loop):
response = yield from websession.get( response = yield from websession.get(
@ -105,14 +104,9 @@ class MjpegCamera(Camera):
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.error('Timeout getting camera image') _LOGGER.error('Timeout getting camera image')
except (aiohttp.errors.ClientError, except aiohttp.ClientError as err:
aiohttp.errors.ClientDisconnectedError) as err:
_LOGGER.error('Error getting new camera image: %s', err) _LOGGER.error('Error getting new camera image: %s', err)
finally:
if response is not None:
yield from response.release()
def camera_image(self): def camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
if self._username and self._password: if self._username and self._password:
@ -140,7 +134,7 @@ class MjpegCamera(Camera):
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
stream_coro = websession.get(self._mjpeg_url, auth=self._auth) stream_coro = websession.get(self._mjpeg_url, auth=self._auth)
yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro) yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
@property @property
def name(self): def name(self):

View File

@ -19,7 +19,7 @@ from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA) Camera, PLATFORM_SCHEMA)
from homeassistant.helpers.aiohttp_client import ( from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_create_clientsession, async_get_clientsession, async_create_clientsession,
async_aiohttp_proxy_stream) async_aiohttp_proxy_web)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.util.async import run_coroutine_threadsafe
@ -74,7 +74,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
'version': '1', 'version': '1',
'query': 'SYNO.' 'query': 'SYNO.'
} }
query_req = None
try: try:
with async_timeout.timeout(timeout, loop=hass.loop): with async_timeout.timeout(timeout, loop=hass.loop):
query_req = yield from websession_init.get( query_req = yield from websession_init.get(
@ -88,14 +87,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
camera_path = query_resp['data'][CAMERA_API]['path'] camera_path = query_resp['data'][CAMERA_API]['path']
streaming_path = query_resp['data'][STREAMING_API]['path'] streaming_path = query_resp['data'][STREAMING_API]['path']
except (asyncio.TimeoutError, aiohttp.errors.ClientError): except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", syno_api_url) _LOGGER.exception("Error on %s", syno_api_url)
return False return False
finally:
if query_req is not None:
yield from query_req.release()
# Authticate to NAS to get a session id # Authticate to NAS to get a session id
syno_auth_url = SYNO_API_URL.format( syno_auth_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, auth_path) config.get(CONF_URL), WEBAPI_PATH, auth_path)
@ -128,13 +123,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
syno_camera_url, syno_camera_url,
params=camera_payload params=camera_payload
) )
except (asyncio.TimeoutError, aiohttp.errors.ClientError): except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", syno_camera_url) _LOGGER.exception("Error on %s", syno_camera_url)
return False return False
camera_resp = yield from camera_req.json() camera_resp = yield from camera_req.json()
cameras = camera_resp['data']['cameras'] cameras = camera_resp['data']['cameras']
yield from camera_req.release()
# add cameras # add cameras
devices = [] devices = []
@ -172,7 +166,6 @@ def get_session_id(hass, websession, username, password, login_url, timeout):
'session': 'SurveillanceStation', 'session': 'SurveillanceStation',
'format': 'sid' 'format': 'sid'
} }
auth_req = None
try: try:
with async_timeout.timeout(timeout, loop=hass.loop): with async_timeout.timeout(timeout, loop=hass.loop):
auth_req = yield from websession.get( auth_req = yield from websession.get(
@ -182,14 +175,10 @@ def get_session_id(hass, websession, username, password, login_url, timeout):
auth_resp = yield from auth_req.json() auth_resp = yield from auth_req.json()
return auth_resp['data']['sid'] return auth_resp['data']['sid']
except (asyncio.TimeoutError, aiohttp.errors.ClientError): except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", login_url) _LOGGER.exception("Error on %s", login_url)
return False return False
finally:
if auth_req is not None:
yield from auth_req.release()
class SynologyCamera(Camera): class SynologyCamera(Camera):
"""An implementation of a Synology NAS based IP camera.""" """An implementation of a Synology NAS based IP camera."""
@ -235,12 +224,11 @@ class SynologyCamera(Camera):
image_url, image_url,
params=image_payload params=image_payload
) )
except (asyncio.TimeoutError, aiohttp.errors.ClientError): except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", image_url) _LOGGER.error("Error fetching %s", image_url)
return None return None
image = yield from response.read() image = yield from response.read()
yield from response.release()
return image return image
@ -260,7 +248,7 @@ class SynologyCamera(Camera):
stream_coro = self._websession.get( stream_coro = self._websession.get(
streaming_url, params=streaming_payload) streaming_url, params=streaming_payload)
yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro) yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
@property @property
def name(self): def name(self):

View File

@ -692,18 +692,16 @@ class ClimateDevice(Entity):
def _convert_for_display(self, temp): def _convert_for_display(self, temp):
"""Convert temperature into preferred units for display purposes.""" """Convert temperature into preferred units for display purposes."""
if (temp is None or not isinstance(temp, Number) or if temp is None or not isinstance(temp, Number):
self.temperature_unit == self.unit_of_measurement):
return temp return temp
if self.temperature_unit != self.unit_of_measurement:
value = convert_temperature(temp, self.temperature_unit, temp = convert_temperature(temp, self.temperature_unit,
self.unit_of_measurement) self.unit_of_measurement)
# Round in the units appropriate # Round in the units appropriate
if self.precision == PRECISION_HALVES: if self.precision == PRECISION_HALVES:
return round(value * 2) / 2.0 return round(temp * 2) / 2.0
elif self.precision == PRECISION_TENTHS: elif self.precision == PRECISION_TENTHS:
return round(value, 1) return round(temp, 1)
else: else:
# PRECISION_WHOLE as a fall back # PRECISION_WHOLE as a fall back
return round(value) return round(temp)

View File

@ -16,11 +16,15 @@ _LOGGER = logging.getLogger(__name__)
STATE_MANUAL = "manual" STATE_MANUAL = "manual"
STATE_BOOST = "boost" STATE_BOOST = "boost"
STATE_COMFORT = "comfort"
STATE_LOWERING = "lowering"
HM_STATE_MAP = { HM_STATE_MAP = {
"AUTO_MODE": STATE_AUTO, "AUTO_MODE": STATE_AUTO,
"MANU_MODE": STATE_MANUAL, "MANU_MODE": STATE_MANUAL,
"BOOST_MODE": STATE_BOOST, "BOOST_MODE": STATE_BOOST,
"COMFORT_MODE": STATE_COMFORT,
"LOWERING_MODE": STATE_LOWERING
} }
HM_TEMP_MAP = [ HM_TEMP_MAP = [

View File

@ -1,40 +1,40 @@
"""tado component to create a climate device for each zone.""" """
Tado component to create a climate device for each zone.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.tado/
"""
import logging import logging
from homeassistant.const import TEMP_CELSIUS from homeassistant.const import TEMP_CELSIUS
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate import ( from homeassistant.const import ATTR_TEMPERATURE
ClimateDevice) from homeassistant.components.tado import DATA_TADO
from homeassistant.const import (
ATTR_TEMPERATURE)
from homeassistant.components.tado import (
DATA_TADO)
CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Default mytado mode
CONST_MODE_OFF = "OFF" # Switch off heating in a zone
# When we change the temperature setting, we need an overlay mode
# wait until tado changes the mode automatic
CONST_OVERLAY_TADO_MODE = "TADO_MODE"
# the user has change the temperature or mode manually
CONST_OVERLAY_MANUAL = "MANUAL"
# the temperature will be reset after a timespan
CONST_OVERLAY_TIMER = "TIMER"
OPERATION_LIST = {
CONST_OVERLAY_MANUAL: "Manual",
CONST_OVERLAY_TIMER: "Timer",
CONST_OVERLAY_TADO_MODE: "Tado mode",
CONST_MODE_SMART_SCHEDULE: "Smart schedule",
CONST_MODE_OFF: "Off"}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONST_MODE_SMART_SCHEDULE = 'SMART_SCHEDULE' # Default mytado mode
CONST_MODE_OFF = 'OFF' # Switch off heating in a zone
# When we change the temperature setting, we need an overlay mode
# wait until tado changes the mode automatic
CONST_OVERLAY_TADO_MODE = 'TADO_MODE'
# the user has change the temperature or mode manually
CONST_OVERLAY_MANUAL = 'MANUAL'
# the temperature will be reset after a timespan
CONST_OVERLAY_TIMER = 'TIMER'
OPERATION_LIST = {
CONST_OVERLAY_MANUAL: 'Manual',
CONST_OVERLAY_TIMER: 'Timer',
CONST_OVERLAY_TADO_MODE: 'Tado mode',
CONST_MODE_SMART_SCHEDULE: 'Smart schedule',
CONST_MODE_OFF: 'Off',
}
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the climate platform.""" """Set up the Tado climate platform."""
# get the PyTado object from the hub component
tado = hass.data[DATA_TADO] tado = hass.data[DATA_TADO]
try: try:
@ -45,10 +45,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
climate_devices = [] climate_devices = []
for zone in zones: for zone in zones:
climate_devices.append(create_climate_device(tado, hass, climate_devices.append(create_climate_device(
zone, tado, hass, zone, zone['name'], zone['id']))
zone['name'],
zone['id']))
if len(climate_devices) > 0: if len(climate_devices) > 0:
add_devices(climate_devices, True) add_devices(climate_devices, True)
@ -58,13 +56,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
def create_climate_device(tado, hass, zone, name, zone_id): def create_climate_device(tado, hass, zone, name, zone_id):
"""Create a climate device.""" """Create a Tado climate device."""
capabilities = tado.get_capabilities(zone_id) capabilities = tado.get_capabilities(zone_id)
unit = TEMP_CELSIUS unit = TEMP_CELSIUS
min_temp = float(capabilities["temperatures"]["celsius"]["min"]) min_temp = float(capabilities['temperatures']['celsius']['min'])
max_temp = float(capabilities["temperatures"]["celsius"]["max"]) max_temp = float(capabilities['temperatures']['celsius']['max'])
ac_mode = capabilities["type"] != "HEATING" ac_mode = capabilities['type'] != 'HEATING'
data_id = 'zone {} {}'.format(name, zone_id) data_id = 'zone {} {}'.format(name, zone_id)
device = TadoClimate(tado, device = TadoClimate(tado,
@ -74,10 +72,10 @@ def create_climate_device(tado, hass, zone, name, zone_id):
ac_mode) ac_mode)
tado.add_sensor(data_id, { tado.add_sensor(data_id, {
"id": zone_id, 'id': zone_id,
"zone": zone, 'zone': zone,
"name": name, 'name': name,
"climate": device 'climate': device
}) })
return device return device
@ -89,7 +87,7 @@ class TadoClimate(ClimateDevice):
def __init__(self, store, zone_name, zone_id, data_id, def __init__(self, store, zone_name, zone_id, data_id,
min_temp, max_temp, ac_mode, min_temp, max_temp, ac_mode,
tolerance=0.3): tolerance=0.3):
"""Initialization of TadoClimate device.""" """Initialization of Tado climate device."""
self._store = store self._store = store
self._data_id = data_id self._data_id = data_id
@ -202,8 +200,7 @@ class TadoClimate(ClimateDevice):
data = self._store.get_data(self._data_id) data = self._store.get_data(self._data_id)
if data is None: if data is None:
_LOGGER.debug('Recieved no data for zone %s', _LOGGER.debug("Recieved no data for zone %s", self.zone_name)
self.zone_name)
return return
if 'sensorDataPoints' in data: if 'sensorDataPoints' in data:
@ -232,11 +229,11 @@ class TadoClimate(ClimateDevice):
if 'tadoMode' in data: if 'tadoMode' in data:
mode = data['tadoMode'] mode = data['tadoMode']
self._is_away = mode == "AWAY" self._is_away = mode == 'AWAY'
if 'setting' in data: if 'setting' in data:
power = data['setting']['power'] power = data['setting']['power']
if power == "OFF": if power == 'OFF':
self._current_operation = CONST_MODE_OFF self._current_operation = CONST_MODE_OFF
self._device_is_active = False self._device_is_active = False
else: else:
@ -249,48 +246,47 @@ class TadoClimate(ClimateDevice):
overlay = False overlay = False
termination = "" termination = ""
# if you set mode manualy to off, there will be an overlay # If you set mode manualy to off, there will be an overlay
# and a termination, but we want to see the mode "OFF" # and a termination, but we want to see the mode "OFF"
if overlay and self._device_is_active: if overlay and self._device_is_active:
# there is an overlay the device is on # There is an overlay the device is on
self._overlay_mode = termination self._overlay_mode = termination
self._current_operation = termination self._current_operation = termination
else: else:
# there is no overlay, the mode will always be # There is no overlay, the mode will always be
# "SMART_SCHEDULE" # "SMART_SCHEDULE"
self._overlay_mode = CONST_MODE_SMART_SCHEDULE self._overlay_mode = CONST_MODE_SMART_SCHEDULE
self._current_operation = CONST_MODE_SMART_SCHEDULE self._current_operation = CONST_MODE_SMART_SCHEDULE
def _control_heating(self): def _control_heating(self):
"""Send new target temperature to mytado.""" """Send new target temperature to mytado."""
if not self._active and None not in (self._cur_temp, if not self._active and None not in (
self._target_temp): self._cur_temp, self._target_temp):
self._active = True self._active = True
_LOGGER.info('Obtained current and target temperature. ' _LOGGER.info("Obtained current and target temperature. "
'tado thermostat active.') "Tado thermostat active")
if not self._active or self._current_operation == self._overlay_mode: if not self._active or self._current_operation == self._overlay_mode:
return return
if self._current_operation == CONST_MODE_SMART_SCHEDULE: if self._current_operation == CONST_MODE_SMART_SCHEDULE:
_LOGGER.info('Switching mytado.com to SCHEDULE (default) ' _LOGGER.info("Switching mytado.com to SCHEDULE (default) "
'for zone %s', self.zone_name) "for zone %s", self.zone_name)
self._store.reset_zone_overlay(self.zone_id) self._store.reset_zone_overlay(self.zone_id)
self._overlay_mode = self._current_operation self._overlay_mode = self._current_operation
return return
if self._current_operation == CONST_MODE_OFF: if self._current_operation == CONST_MODE_OFF:
_LOGGER.info('Switching mytado.com to OFF for zone %s', _LOGGER.info("Switching mytado.com to OFF for zone %s",
self.zone_name) self.zone_name)
self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL) self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL)
self._overlay_mode = self._current_operation self._overlay_mode = self._current_operation
return return
_LOGGER.info('Switching mytado.com to %s mode for zone %s', _LOGGER.info("Switching mytado.com to %s mode for zone %s",
self._current_operation, self.zone_name) self._current_operation, self.zone_name)
self._store.set_zone_overlay(self.zone_id, self._store.set_zone_overlay(
self._current_operation, self.zone_id, self._current_operation, self._target_temp)
self._target_temp)
self._overlay_mode = self._current_operation self._overlay_mode = self._current_operation

View File

@ -19,7 +19,6 @@ DEPENDENCIES = ['wink']
STATE_AUX = 'aux' STATE_AUX = 'aux'
STATE_ECO = 'eco' STATE_ECO = 'eco'
STATE_FAN = 'fan' STATE_FAN = 'fan'
SPEED_LOWEST = 'lowest'
SPEED_LOW = 'low' SPEED_LOW = 'low'
SPEED_MEDIUM = 'medium' SPEED_MEDIUM = 'medium'
SPEED_HIGH = 'high' SPEED_HIGH = 'high'
@ -400,7 +399,7 @@ class WinkAC(WinkDevice, ClimateDevice):
op_list.append(STATE_COOL) op_list.append(STATE_COOL)
if 'auto_eco' in modes: if 'auto_eco' in modes:
op_list.append(STATE_ECO) op_list.append(STATE_ECO)
if 'fan_eco' in modes: if 'fan_only' in modes:
op_list.append(STATE_FAN) op_list.append(STATE_FAN)
return op_list return op_list
@ -439,9 +438,7 @@ class WinkAC(WinkDevice, ClimateDevice):
def current_fan_mode(self): def current_fan_mode(self):
"""Return the current fan mode.""" """Return the current fan mode."""
speed = self.wink.current_fan_speed() speed = self.wink.current_fan_speed()
if speed <= 0.3 and speed >= 0.0: if speed <= 0.4 and speed > 0.3:
return SPEED_LOWEST
elif speed <= 0.5 and speed > 0.3:
return SPEED_LOW return SPEED_LOW
elif speed <= 0.8 and speed > 0.5: elif speed <= 0.8 and speed > 0.5:
return SPEED_MEDIUM return SPEED_MEDIUM
@ -453,14 +450,12 @@ class WinkAC(WinkDevice, ClimateDevice):
@property @property
def fan_list(self): def fan_list(self):
"""List of available fan modes.""" """List of available fan modes."""
return [SPEED_LOWEST, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
def set_fan_mode(self, mode): def set_fan_mode(self, mode):
"""Set fan speed.""" """Set fan speed."""
if mode == SPEED_LOWEST: if mode == SPEED_LOW:
speed = 0.3 speed = 0.4
elif mode == SPEED_LOW:
speed = 0.5
elif mode == SPEED_MEDIUM: elif mode == SPEED_MEDIUM:
speed = 0.8 speed = 0.8
elif mode == SPEED_HIGH: elif mode == SPEED_HIGH:

View File

@ -112,6 +112,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
_LOGGER.debug("Setpoint is 0, setting default to " _LOGGER.debug("Setpoint is 0, setting default to "
"current_temperature=%s", "current_temperature=%s",
self._current_temperature) self._current_temperature)
if self._current_temperature is not None:
self._target_temperature = ( self._target_temperature = (
round((float(self._current_temperature)), 1)) round((float(self._current_temperature)), 1))
else: else:

View File

@ -47,6 +47,7 @@ class VeraCover(VeraDevice, CoverDevice):
def set_cover_position(self, position, **kwargs): def set_cover_position(self, position, **kwargs):
"""Move the cover to a specific position.""" """Move the cover to a specific position."""
self.vera_device.set_level(position) self.vera_device.set_level(position)
self.schedule_update_ha_state()
@property @property
def is_closed(self): def is_closed(self):
@ -60,11 +61,14 @@ class VeraCover(VeraDevice, CoverDevice):
def open_cover(self, **kwargs): def open_cover(self, **kwargs):
"""Open the cover.""" """Open the cover."""
self.vera_device.open() self.vera_device.open()
self.schedule_update_ha_state()
def close_cover(self, **kwargs): def close_cover(self, **kwargs):
"""Close the cover.""" """Close the cover."""
self.vera_device.close() self.vera_device.close()
self.schedule_update_ha_state()
def stop_cover(self, **kwargs): def stop_cover(self, **kwargs):
"""Stop the cover.""" """Stop the cover."""
self.vera_device.stop() self.vera_device.stop()
self.schedule_update_ha_state()

View File

@ -20,12 +20,13 @@ _LOGGER = logging.getLogger(__name__)
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
def get_device(values, **kwargs): def get_device(values, node_config, **kwargs):
"""Create zwave entity device.""" """Create zwave entity device."""
invert_buttons = node_config.get(zwave.CONF_INVERT_OPENCLOSE_BUTTONS)
if (values.primary.command_class == if (values.primary.command_class ==
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
and values.primary.index == 0): and values.primary.index == 0):
return ZwaveRollershutter(values) return ZwaveRollershutter(values, invert_buttons)
elif (values.primary.command_class in [ elif (values.primary.command_class in [
zwave.const.COMMAND_CLASS_SWITCH_BINARY, zwave.const.COMMAND_CLASS_SWITCH_BINARY,
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]): zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]):
@ -36,13 +37,14 @@ def get_device(values, **kwargs):
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
"""Representation of an Zwave roller shutter.""" """Representation of an Zwave roller shutter."""
def __init__(self, values): def __init__(self, values, invert_buttons):
"""Initialize the zwave rollershutter.""" """Initialize the zwave rollershutter."""
ZWaveDeviceEntity.__init__(self, values, DOMAIN) ZWaveDeviceEntity.__init__(self, values, DOMAIN)
# pylint: disable=no-member # pylint: disable=no-member
self._open_id = None self._open_id = None
self._close_id = None self._close_id = None
self._current_position = None self._current_position = None
self._invert_buttons = invert_buttons
self._workaround = workaround.get_device_mapping(values.primary) self._workaround = workaround.get_device_mapping(values.primary)
if self._workaround: if self._workaround:
@ -56,10 +58,9 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
if self.values.open and self.values.close and \ if self.values.open and self.values.close and \
self._open_id is None and self._close_id is None: self._open_id is None and self._close_id is None:
if self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE: if self._invert_buttons:
self._open_id = self.values.close.value_id self._open_id = self.values.close.value_id
self._close_id = self.values.open.value_id self._close_id = self.values.open.value_id
self._workaround = None
else: else:
self._open_id = self.values.open.value_id self._open_id = self.values.open.value_id
self._close_id = self.values.close.value_id self._close_id = self.values.close.value_id

View File

@ -553,7 +553,6 @@ class Device(Entity):
# bytes like 00 get truncates to 0, API needs full bytes # bytes like 00 get truncates to 0, API needs full bytes
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes]) oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
url = 'http://api.macvendors.com/' + oui url = 'http://api.macvendors.com/' + oui
resp = None
try: try:
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
@ -570,13 +569,9 @@ class Device(Entity):
# in the 'known_devices.yaml' file which only happens # in the 'known_devices.yaml' file which only happens
# the first time the device is seen. # the first time the device is seen.
return 'unknown' return 'unknown'
except (asyncio.TimeoutError, aiohttp.errors.ClientError, except (asyncio.TimeoutError, aiohttp.ClientError):
aiohttp.errors.ClientDisconnectedError):
# same as above # same as above
return 'unknown' return 'unknown'
finally:
if resp is not None:
yield from resp.release()
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):

View File

@ -18,6 +18,7 @@ from homeassistant.components.device_tracker import ( # NOQA
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
URL = '/api/locative'
def setup_scanner(hass, config, see, discovery_info=None): def setup_scanner(hass, config, see, discovery_info=None):
@ -30,7 +31,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
class LocativeView(HomeAssistantView): class LocativeView(HomeAssistantView):
"""View to handle locative requests.""" """View to handle locative requests."""
url = '/api/locative' url = URL
name = 'api:locative' name = 'api:locative'
def __init__(self, see): def __init__(self, see):

View File

@ -16,11 +16,11 @@ _LOGGER = logging.getLogger(__name__)
def setup_scanner(hass, config, see, discovery_info=None): def setup_scanner(hass, config, see, discovery_info=None):
"""Setup the MySensors tracker.""" """Setup the MySensors tracker."""
def mysensors_callback(gateway, node_id): def mysensors_callback(gateway, msg):
"""Callback for mysensors platform.""" """Callback for mysensors platform."""
node = gateway.sensors[node_id] node = gateway.sensors[msg.node_id]
if node.sketch_name is None: if node.sketch_name is None:
_LOGGER.info('No sketch_name: node %s', node_id) _LOGGER.info('No sketch_name: node %s', msg.node_id)
return return
pres = gateway.const.Presentation pres = gateway.const.Presentation
@ -37,12 +37,12 @@ def setup_scanner(hass, config, see, discovery_info=None):
'latitude,longitude,altitude', position) 'latitude,longitude,altitude', position)
continue continue
name = '{} {} {}'.format( name = '{} {} {}'.format(
node.sketch_name, node_id, child.id) node.sketch_name, msg.node_id, child.id)
attr = { attr = {
mysensors.ATTR_CHILD_ID: child.id, mysensors.ATTR_CHILD_ID: child.id,
mysensors.ATTR_DESCRIPTION: child.description, mysensors.ATTR_DESCRIPTION: child.description,
mysensors.ATTR_DEVICE: gateway.device, mysensors.ATTR_DEVICE: gateway.device,
mysensors.ATTR_NODE_ID: node_id, mysensors.ATTR_NODE_ID: msg.node_id,
} }
see( see(
dev_id=slugify(name), dev_id=slugify(name),

View File

@ -19,7 +19,7 @@ from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pysnmp==4.3.4'] REQUIREMENTS = ['pysnmp==4.3.5']
CONF_COMMUNITY = 'community' CONF_COMMUNITY = 'community'
CONF_AUTHKEY = 'authkey' CONF_AUTHKEY = 'authkey'
@ -125,7 +125,10 @@ class SnmpScanner(DeviceScanner):
for resrow in restable: for resrow in restable:
for _, val in resrow: for _, val in resrow:
try:
mac = binascii.hexlify(val.asOctets()).decode('utf-8') mac = binascii.hexlify(val.asOctets()).decode('utf-8')
except AttributeError:
continue
_LOGGER.debug("Found MAC %s", mac) _LOGGER.debug("Found MAC %s", mac)
mac = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)]) mac = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)])
devices.append({'mac': mac}) devices.append({'mac': mac})

View File

@ -105,8 +105,6 @@ class TadoDeviceScanner(DeviceScanner):
_LOGGER.debug("Requesting Tado") _LOGGER.debug("Requesting Tado")
last_results = [] last_results = []
response = None
tado_json = None
try: try:
with async_timeout.timeout(10, loop=self.hass.loop): with async_timeout.timeout(10, loop=self.hass.loop):
@ -127,14 +125,10 @@ class TadoDeviceScanner(DeviceScanner):
tado_json = yield from response.json() tado_json = yield from response.json()
except (asyncio.TimeoutError, aiohttp.errors.ClientError): except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Cannot load Tado data") _LOGGER.error("Cannot load Tado data")
return False return False
finally:
if response is not None:
yield from response.release()
# Without a home_id, we fetched an URL where the mobile devices can be # Without a home_id, we fetched an URL where the mobile devices can be
# found under the mobileDevices key. # found under the mobileDevices key.
if 'mobileDevices' in tado_json: if 'mobileDevices' in tado_json:

View File

@ -101,7 +101,6 @@ class UPCDeviceScanner(DeviceScanner):
@asyncio.coroutine @asyncio.coroutine
def async_login(self): def async_login(self):
"""Login into firmware and get first token.""" """Login into firmware and get first token."""
response = None
try: try:
# get first token # get first token
with async_timeout.timeout(10, loop=self.hass.loop): with async_timeout.timeout(10, loop=self.hass.loop):
@ -110,6 +109,7 @@ class UPCDeviceScanner(DeviceScanner):
) )
yield from response.text() yield from response.text()
self.token = response.cookies['sessionToken'].value self.token = response.cookies['sessionToken'].value
# login # login
@ -119,18 +119,12 @@ class UPCDeviceScanner(DeviceScanner):
}) })
# successfull? # successfull?
if data is not None: return data is not None
return True
return False
except (asyncio.TimeoutError, aiohttp.errors.ClientError): except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Can not load login page from %s", self.host) _LOGGER.error("Can not load login page from %s", self.host)
return False return False
finally:
if response is not None:
yield from response.release()
@asyncio.coroutine @asyncio.coroutine
def _async_ws_function(self, function, additional_form=None): def _async_ws_function(self, function, additional_form=None):
"""Execute a command on UPC firmware webservice.""" """Execute a command on UPC firmware webservice."""
@ -142,8 +136,7 @@ class UPCDeviceScanner(DeviceScanner):
if additional_form: if additional_form:
form_data.update(additional_form) form_data.update(additional_form)
redirects = True if function != CMD_DEVICES else False redirects = function != CMD_DEVICES
response = None
try: try:
with async_timeout.timeout(10, loop=self.hass.loop): with async_timeout.timeout(10, loop=self.hass.loop):
response = yield from self.websession.post( response = yield from self.websession.post(
@ -163,10 +156,6 @@ class UPCDeviceScanner(DeviceScanner):
self.token = response.cookies['sessionToken'].value self.token = response.cookies['sessionToken'].value
return (yield from response.text()) return (yield from response.text())
except (asyncio.TimeoutError, aiohttp.errors.ClientError): except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Error on %s", function) _LOGGER.error("Error on %s", function)
self.token = None self.token = None
finally:
if response is not None:
yield from response.release()

View File

@ -153,7 +153,7 @@ def setup(hass, config):
sw_path = "service_worker.js" sw_path = "service_worker.js"
hass.http.register_static_path("/service_worker.js", hass.http.register_static_path("/service_worker.js",
os.path.join(STATIC_PATH, sw_path), 0) os.path.join(STATIC_PATH, sw_path), False)
hass.http.register_static_path("/robots.txt", hass.http.register_static_path("/robots.txt",
os.path.join(STATIC_PATH, "robots.txt")) os.path.join(STATIC_PATH, "robots.txt"))
hass.http.register_static_path("/static", STATIC_PATH) hass.http.register_static_path("/static", STATIC_PATH)

View File

@ -3,8 +3,8 @@
FINGERPRINTS = { FINGERPRINTS = {
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0", "compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
"core.js": "5d08475f03adb5969bd31855d5ca0cfd", "core.js": "5d08475f03adb5969bd31855d5ca0cfd",
"frontend.html": "53c45b837a3bcae7cfb9ef4a5919844f", "frontend.html": "feaf3e9453eca239f29eb10e7645a84f",
"mdi.html": "4921d26f29dc148c3e8bd5bcd8ce5822", "mdi.html": "989f02c51eba561dc32b9ecc628a84b3",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-config.html": "6dcb246cd356307a638f81c4f89bf9b3", "panels/ha-panel-config.html": "6dcb246cd356307a638f81c4f89bf9b3",
"panels/ha-panel-dev-event.html": "1f169700c2345785855b1d7919d12326", "panels/ha-panel-dev-event.html": "1f169700c2345785855b1d7919d12326",
@ -12,9 +12,9 @@ FINGERPRINTS = {
"panels/ha-panel-dev-service.html": "0fe8e6acdccf2dc3d1ae657b2c7f2df0", "panels/ha-panel-dev-service.html": "0fe8e6acdccf2dc3d1ae657b2c7f2df0",
"panels/ha-panel-dev-state.html": "48d37db4a1d6708314ded1d624d0f4d4", "panels/ha-panel-dev-state.html": "48d37db4a1d6708314ded1d624d0f4d4",
"panels/ha-panel-dev-template.html": "6f353392d68574fbc5af188bca44d0ae", "panels/ha-panel-dev-template.html": "6f353392d68574fbc5af188bca44d0ae",
"panels/ha-panel-history.html": "bfd5f929d5aa9cefdd799ec37624efa1", "panels/ha-panel-history.html": "6945cebe5d8075aae560d2c4246b063f",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "a1fc2b5d739bedb9d87e4da4cd929a71", "panels/ha-panel-logbook.html": "a1fc2b5d739bedb9d87e4da4cd929a71",
"panels/ha-panel-map.html": "9aa065b1908089f3bb5af7fdf9485be5", "panels/ha-panel-map.html": "e3c7a54f90dd4269d7e53cdcd96514c6",
"websocket_test.html": "575de64b431fe11c3785bf96d7813450" "websocket_test.html": "575de64b431fe11c3785bf96d7813450"
} }

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit f4c59e1eff3223262c198a29cf70c62572de019b Subproject commit abbdc6f055524c5d3ed0bb50e35400fed40d573f

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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)

View File

@ -22,7 +22,7 @@ from homeassistant.helpers.event import track_time_interval
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
DOMAIN = 'homematic' DOMAIN = 'homematic'
REQUIREMENTS = ["pyhomematic==0.1.22"] REQUIREMENTS = ["pyhomematic==0.1.24"]
SCAN_INTERVAL_HUB = timedelta(seconds=300) SCAN_INTERVAL_HUB = timedelta(seconds=300)
SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) SCAN_INTERVAL_VARIABLES = timedelta(seconds=30)
@ -82,7 +82,12 @@ HM_ATTRIBUTE_SUPPORT = {
'RSSI_DEVICE': ['rssi', {}], 'RSSI_DEVICE': ['rssi', {}],
'VALVE_STATE': ['valve', {}], 'VALVE_STATE': ['valve', {}],
'BATTERY_STATE': ['battery', {}], 'BATTERY_STATE': ['battery', {}],
'CONTROL_MODE': ['mode', {0: 'Auto', 1: 'Manual', 2: 'Away', 3: 'Boost'}], 'CONTROL_MODE': ['mode', {0: 'Auto',
1: 'Manual',
2: 'Away',
3: 'Boost',
4: 'Comfort',
5: 'Lowering'}],
'POWER': ['power', {}], 'POWER': ['power', {}],
'CURRENT': ['current', {}], 'CURRENT': ['current', {}],
'VOLTAGE': ['voltage', {}], 'VOLTAGE': ['voltage', {}],

View File

@ -9,7 +9,6 @@ import json
import logging import logging
import ssl import ssl
from ipaddress import ip_network from ipaddress import ip_network
from pathlib import Path
import os import os
import voluptuous as vol import voluptuous as vol
@ -31,11 +30,13 @@ from .const import (
KEY_USE_X_FORWARDED_FOR, KEY_TRUSTED_NETWORKS, KEY_USE_X_FORWARDED_FOR, KEY_TRUSTED_NETWORKS,
KEY_BANS_ENABLED, KEY_LOGIN_THRESHOLD, KEY_BANS_ENABLED, KEY_LOGIN_THRESHOLD,
KEY_DEVELOPMENT, KEY_AUTHENTICATED) KEY_DEVELOPMENT, KEY_AUTHENTICATED)
from .static import FILE_SENDER, CACHING_FILE_SENDER, staticresource_middleware from .static import (
staticresource_middleware, CachingFileResponse, CachingStaticResource)
from .util import get_real_ip from .util import get_real_ip
REQUIREMENTS = ['aiohttp_cors==0.5.2']
DOMAIN = 'http' DOMAIN = 'http'
REQUIREMENTS = ('aiohttp_cors==0.5.0',)
CONF_API_PASSWORD = 'api_password' CONF_API_PASSWORD = 'api_password'
CONF_SERVER_HOST = 'server_host' CONF_SERVER_HOST = 'server_host'
@ -187,7 +188,7 @@ class HomeAssistantWSGI(object):
if is_ban_enabled: if is_ban_enabled:
middlewares.insert(0, ban_middleware) middlewares.insert(0, ban_middleware)
self.app = web.Application(middlewares=middlewares, loop=hass.loop) self.app = web.Application(middlewares=middlewares)
self.app['hass'] = hass self.app['hass'] = hass
self.app[KEY_USE_X_FORWARDED_FOR] = use_x_forwarded_for self.app[KEY_USE_X_FORWARDED_FOR] = use_x_forwarded_for
self.app[KEY_TRUSTED_NETWORKS] = trusted_networks self.app[KEY_TRUSTED_NETWORKS] = trusted_networks
@ -255,31 +256,39 @@ class HomeAssistantWSGI(object):
self.app.router.add_route('GET', url, redirect) self.app.router.add_route('GET', url, redirect)
def register_static_path(self, url_root, path, cache_length=31): def register_static_path(self, url_path, path, cache_headers=True):
"""Register a folder to serve as a static path. """Register a folder or file to serve as a static path."""
Specify optional cache length of asset in days.
"""
if os.path.isdir(path): if os.path.isdir(path):
self.app.router.add_static(url_root, path) if cache_headers:
resource = CachingStaticResource
else:
resource = web.StaticResource
self.app.router.register_resource(resource(url_path, path))
return return
filepath = Path(path) if cache_headers:
@asyncio.coroutine @asyncio.coroutine
def serve_file(request): def serve_file(request):
"""Serve file from disk.""" """Serve file from disk."""
res = yield from CACHING_FILE_SENDER.send(request, filepath) return CachingFileResponse(path)
return res else:
@asyncio.coroutine
def serve_file(request):
"""Serve file from disk."""
return web.FileResponse(path)
# aiohttp supports regex matching for variables. Using that as temp # aiohttp supports regex matching for variables. Using that as temp
# to work around cache busting MD5. # to work around cache busting MD5.
# Turns something like /static/dev-panel.html into # Turns something like /static/dev-panel.html into
# /static/{filename:dev-panel(-[a-z0-9]{32}|)\.html} # /static/{filename:dev-panel(-[a-z0-9]{32}|)\.html}
base, ext = url_root.rsplit('.', 1) base, ext = os.path.splitext(url_path)
if ext:
base, file = base.rsplit('/', 1) base, file = base.rsplit('/', 1)
regex = r"{}(-[a-z0-9]{{32}}|)\.{}".format(file, ext) regex = r"{}(-[a-z0-9]{{32}}|){}".format(file, ext)
url_pattern = "{}/{{filename:{}}}".format(base, regex) url_pattern = "{}/{{filename:{}}}".format(base, regex)
else:
url_pattern = url_path
self.app.router.add_route('GET', url_pattern, serve_file) self.app.router.add_route('GET', url_pattern, serve_file)
@ -318,7 +327,7 @@ class HomeAssistantWSGI(object):
# re-register all redirects, views, static paths. # re-register all redirects, views, static paths.
self.app._frozen = True # pylint: disable=protected-access self.app._frozen = True # pylint: disable=protected-access
self._handler = self.app.make_handler() self._handler = self.app.make_handler(loop=self.hass.loop)
try: try:
self.server = yield from self.hass.loop.create_server( self.server = yield from self.hass.loop.create_server(
@ -365,8 +374,7 @@ class HomeAssistantView(object):
def file(self, request, fil): def file(self, request, fil):
"""Return a file.""" """Return a file."""
assert isinstance(fil, str), 'only string paths allowed' assert isinstance(fil, str), 'only string paths allowed'
response = yield from FILE_SENDER.send(request, Path(fil)) return web.FileResponse(fil)
return response
def register(self, router): def register(self, router):
"""Register the view with a router.""" """Register the view with a router."""

View File

@ -3,14 +3,45 @@ import asyncio
import re import re
from aiohttp import hdrs from aiohttp import hdrs
from aiohttp.file_sender import FileSender from aiohttp.web import FileResponse
from aiohttp.web_exceptions import HTTPNotFound
from aiohttp.web_urldispatcher import StaticResource from aiohttp.web_urldispatcher import StaticResource
from yarl import unquote
from .const import KEY_DEVELOPMENT from .const import KEY_DEVELOPMENT
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE) _FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
class CachingFileSender(FileSender): class CachingStaticResource(StaticResource):
"""Static Resource handler that will add cache headers."""
@asyncio.coroutine
def _handle(self, request):
filename = unquote(request.match_info['filename'])
try:
# PyLint is wrong about resolve not being a member.
# pylint: disable=no-member
filepath = self._directory.joinpath(filename).resolve()
if not self._follow_symlinks:
filepath.relative_to(self._directory)
except (ValueError, FileNotFoundError) as error:
# relatively safe
raise HTTPNotFound() from error
except Exception as error:
# perm error or other kind!
request.app.logger.exception(error)
raise HTTPNotFound() from error
if filepath.is_dir():
return (yield from super()._handle(request))
elif filepath.is_file():
return CachingFileResponse(filepath, chunk_size=self._chunk_size)
else:
raise HTTPNotFound
class CachingFileResponse(FileResponse):
"""FileSender class that caches output if not in dev mode.""" """FileSender class that caches output if not in dev mode."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -20,46 +51,34 @@ class CachingFileSender(FileSender):
orig_sendfile = self._sendfile orig_sendfile = self._sendfile
@asyncio.coroutine @asyncio.coroutine
def sendfile(request, resp, fobj, count): def sendfile(request, fobj, count):
"""Sendfile that includes a cache header.""" """Sendfile that includes a cache header."""
if not request.app[KEY_DEVELOPMENT]: if not request.app[KEY_DEVELOPMENT]:
cache_time = 31 * 86400 # = 1 month cache_time = 31 * 86400 # = 1 month
resp.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format( self.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format(
cache_time) cache_time)
yield from orig_sendfile(request, resp, fobj, count) yield from orig_sendfile(request, fobj, count)
# Overwriting like this because __init__ can change implementation. # Overwriting like this because __init__ can change implementation.
self._sendfile = sendfile self._sendfile = sendfile
FILE_SENDER = FileSender()
CACHING_FILE_SENDER = CachingFileSender()
@asyncio.coroutine @asyncio.coroutine
def staticresource_middleware(app, handler): def staticresource_middleware(app, handler):
"""Enhance StaticResourceHandler middleware. """Middleware to strip out fingerprint from fingerprinted assets."""
Adds gzip encoding and fingerprinting matching.
"""
inst = getattr(handler, '__self__', None)
if not isinstance(inst, StaticResource):
return handler
# pylint: disable=protected-access
inst._file_sender = CACHING_FILE_SENDER
@asyncio.coroutine @asyncio.coroutine
def static_middleware_handler(request): def static_middleware_handler(request):
"""Strip out fingerprints from resource names.""" """Strip out fingerprints from resource names."""
if not request.path.startswith('/static/'):
return handler(request)
fingerprinted = _FINGERPRINT.match(request.match_info['filename']) fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
if fingerprinted: if fingerprinted:
request.match_info['filename'] = \ request.match_info['filename'] = \
'{}.{}'.format(*fingerprinted.groups()) '{}.{}'.format(*fingerprinted.groups())
resp = yield from handler(request) return handler(request)
return resp
return static_middleware_handler return static_middleware_handler

View File

@ -112,8 +112,6 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity):
params['image_bytes'] = str(b64encode(image), 'utf-8') params['image_bytes'] = str(b64encode(image), 'utf-8')
data = None
request = None
try: try:
with async_timeout.timeout(self.timeout, loop=self.hass.loop): with async_timeout.timeout(self.timeout, loop=self.hass.loop):
request = yield from websession.post( request = yield from websession.post(
@ -127,14 +125,10 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity):
request.status, data.get('error')) request.status, data.get('error'))
return return
except (asyncio.TimeoutError, aiohttp.errors.ClientError): except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Timeout for openalpr api.") _LOGGER.error("Timeout for openalpr api.")
return return
finally:
if request is not None:
yield from request.release()
# processing api data # processing api data
vehicles = 0 vehicles = 0
result = {} result = {}

View File

@ -30,13 +30,11 @@ SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
}) })
DEFAULT_CONFIG = {CONF_INITIAL: DEFAULT_INITIAL}
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
cv.slug: vol.Any({ cv.slug: vol.Any({
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.boolean, vol.Optional(CONF_INITIAL): cv.boolean,
vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_ICON): cv.icon,
}, None) }, None)
}) })
@ -72,13 +70,13 @@ def async_setup(hass, config):
for object_id, cfg in config[DOMAIN].items(): for object_id, cfg in config[DOMAIN].items():
if not cfg: if not cfg:
cfg = DEFAULT_CONFIG cfg = {}
name = cfg.get(CONF_NAME) name = cfg.get(CONF_NAME)
state = cfg.get(CONF_INITIAL) initial = cfg.get(CONF_INITIAL)
icon = cfg.get(CONF_ICON) icon = cfg.get(CONF_ICON)
entities.append(InputBoolean(object_id, name, state, icon)) entities.append(InputBoolean(object_id, name, initial, icon))
if not entities: if not entities:
return False return False
@ -113,11 +111,11 @@ def async_setup(hass, config):
class InputBoolean(ToggleEntity): class InputBoolean(ToggleEntity):
"""Representation of a boolean input.""" """Representation of a boolean input."""
def __init__(self, object_id, name, state, icon): def __init__(self, object_id, name, initial, icon):
"""Initialize a boolean input.""" """Initialize a boolean input."""
self.entity_id = ENTITY_ID_FORMAT.format(object_id) self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self._name = name self._name = name
self._state = state self._state = initial
self._icon = icon self._icon = icon
@property @property
@ -143,10 +141,12 @@ class InputBoolean(ToggleEntity):
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Called when entity about to be added to hass.""" """Called when entity about to be added to hass."""
state = yield from async_get_last_state(self.hass, self.entity_id) # If not None, we got an initial value.
if not state: if self._state is not None:
return return
self._state = state.state == STATE_ON
state = yield from async_get_last_state(self.hass, self.entity_id)
self._state = state and state.state == STATE_ON
@asyncio.coroutine @asyncio.coroutine
def async_turn_on(self, **kwargs): def async_turn_on(self, **kwargs):

View File

@ -58,10 +58,10 @@ SERVICE_SET_OPTIONS_SCHEMA = vol.Schema({
def _cv_input_select(cfg): def _cv_input_select(cfg):
"""Config validation helper for input select (Voluptuous).""" """Config validation helper for input select (Voluptuous)."""
options = cfg[CONF_OPTIONS] options = cfg[CONF_OPTIONS]
state = cfg.get(CONF_INITIAL, options[0]) initial = cfg.get(CONF_INITIAL)
if state not in options: if initial is not None and initial not in options:
raise vol.Invalid('initial state "{}" is not part of the options: {}' raise vol.Invalid('initial state "{}" is not part of the options: {}'
.format(state, ','.join(options))) .format(initial, ','.join(options)))
return cfg return cfg
@ -117,9 +117,9 @@ def async_setup(hass, config):
for object_id, cfg in config[DOMAIN].items(): for object_id, cfg in config[DOMAIN].items():
name = cfg.get(CONF_NAME) name = cfg.get(CONF_NAME)
options = cfg.get(CONF_OPTIONS) options = cfg.get(CONF_OPTIONS)
state = cfg.get(CONF_INITIAL, options[0]) initial = cfg.get(CONF_INITIAL)
icon = cfg.get(CONF_ICON) icon = cfg.get(CONF_ICON)
entities.append(InputSelect(object_id, name, state, options, icon)) entities.append(InputSelect(object_id, name, initial, options, icon))
if not entities: if not entities:
return False return False
@ -187,22 +187,24 @@ def async_setup(hass, config):
class InputSelect(Entity): class InputSelect(Entity):
"""Representation of a select input.""" """Representation of a select input."""
def __init__(self, object_id, name, state, options, icon): def __init__(self, object_id, name, initial, options, icon):
"""Initialize a select input.""" """Initialize a select input."""
self.entity_id = ENTITY_ID_FORMAT.format(object_id) self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self._name = name self._name = name
self._current_option = state self._current_option = initial
self._options = options self._options = options
self._icon = icon self._icon = icon
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Called when entity about to be added to hass.""" """Called when entity about to be added to hass."""
if self._current_option is not None:
return
state = yield from async_get_last_state(self.hass, self.entity_id) state = yield from async_get_last_state(self.hass, self.entity_id)
if not state: if not state or state.state not in self._options:
return self._current_option = self._options[0]
if state.state not in self._options: else:
return
self._current_option = state.state self._current_option = state.state
@property @property

View File

@ -45,11 +45,10 @@ def _cv_input_slider(cfg):
if minimum >= maximum: if minimum >= maximum:
raise vol.Invalid('Maximum ({}) is not greater than minimum ({})' raise vol.Invalid('Maximum ({}) is not greater than minimum ({})'
.format(minimum, maximum)) .format(minimum, maximum))
state = cfg.get(CONF_INITIAL, minimum) state = cfg.get(CONF_INITIAL)
if state < minimum or state > maximum: if state is not None and (state < minimum or state > maximum):
raise vol.Invalid('Initial value {} not in range {}-{}' raise vol.Invalid('Initial value {} not in range {}-{}'
.format(state, minimum, maximum)) .format(state, minimum, maximum))
cfg[CONF_INITIAL] = state
return cfg return cfg
@ -88,12 +87,12 @@ def async_setup(hass, config):
name = cfg.get(CONF_NAME) name = cfg.get(CONF_NAME)
minimum = cfg.get(CONF_MIN) minimum = cfg.get(CONF_MIN)
maximum = cfg.get(CONF_MAX) maximum = cfg.get(CONF_MAX)
state = cfg.get(CONF_INITIAL, minimum) initial = cfg.get(CONF_INITIAL)
step = cfg.get(CONF_STEP) step = cfg.get(CONF_STEP)
icon = cfg.get(CONF_ICON) icon = cfg.get(CONF_ICON)
unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT) unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT)
entities.append(InputSlider(object_id, name, state, minimum, maximum, entities.append(InputSlider(object_id, name, initial, minimum, maximum,
step, icon, unit)) step, icon, unit))
if not entities: if not entities:
@ -120,12 +119,12 @@ def async_setup(hass, config):
class InputSlider(Entity): class InputSlider(Entity):
"""Represent an slider.""" """Represent an slider."""
def __init__(self, object_id, name, state, minimum, maximum, step, icon, def __init__(self, object_id, name, initial, minimum, maximum, step, icon,
unit): unit):
"""Initialize a select input.""" """Initialize a select input."""
self.entity_id = ENTITY_ID_FORMAT.format(object_id) self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self._name = name self._name = name
self._current_value = state self._current_value = initial
self._minimum = minimum self._minimum = minimum
self._maximum = maximum self._maximum = maximum
self._step = step self._step = step
@ -169,14 +168,17 @@ class InputSlider(Entity):
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Called when entity about to be added to hass.""" """Called when entity about to be added to hass."""
state = yield from async_get_last_state(self.hass, self.entity_id) if self._current_value is not None:
if not state:
return return
num_value = float(state.state) state = yield from async_get_last_state(self.hass, self.entity_id)
if num_value < self._minimum or num_value > self._maximum: value = state and float(state.state)
return
self._current_value = num_value # Check against False because value can be 0
if value is not False and self._minimum < value < self._maximum:
self._current_value = value
else:
self._current_value = self._minimum
@asyncio.coroutine @asyncio.coroutine
def async_select_value(self, value): def async_select_value(self, value):

View File

@ -18,7 +18,7 @@ from homeassistant.components.light import (
PLATFORM_SCHEMA) PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['flux_led==0.15'] REQUIREMENTS = ['flux_led==0.18']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -47,7 +47,7 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
PHUE_CONFIG_FILE = 'phue.conf' PHUE_CONFIG_FILE = 'phue.conf'
SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION | SUPPORT_FLASH) SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION)
SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS) SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS)
SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP) SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP)
SUPPORT_HUE_COLOR = (SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT | SUPPORT_HUE_COLOR = (SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT |
@ -58,6 +58,7 @@ SUPPORT_HUE = {
'Extended color light': SUPPORT_HUE_EXTENDED, 'Extended color light': SUPPORT_HUE_EXTENDED,
'Color light': SUPPORT_HUE_COLOR, 'Color light': SUPPORT_HUE_COLOR,
'Dimmable light': SUPPORT_HUE_DIMMABLE, 'Dimmable light': SUPPORT_HUE_DIMMABLE,
'On/Off plug-in unit': SUPPORT_HUE_ON_OFF,
'Color temperature light': SUPPORT_HUE_COLOR_TEMP 'Color temperature light': SUPPORT_HUE_COLOR_TEMP
} }

View File

@ -175,7 +175,7 @@ class InsteonLocalDimmerDevice(Light):
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
brightness = int(kwargs[ATTR_BRIGHTNESS]) / 255 * 100 brightness = int(kwargs[ATTR_BRIGHTNESS]) / 255 * 100
self.node.on(brightness) self.node.change_level(brightness)
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn device off.""" """Turn device off."""

View File

@ -36,7 +36,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
InsteonPLMDimmerDevice(hass, plm, address, name, dimmable) InsteonPLMDimmerDevice(hass, plm, address, name, dimmable)
) )
hass.async_add_job(async_add_devices(device_list)) async_add_devices(device_list)
class InsteonPLMDimmerDevice(Light): class InsteonPLMDimmerDevice(Light):

View File

@ -7,6 +7,7 @@ https://home-assistant.io/components/light.lifx/
import colorsys import colorsys
import logging import logging
import asyncio import asyncio
import sys
from functools import partial from functools import partial
from datetime import timedelta from datetime import timedelta
@ -51,6 +52,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the LIFX platform.""" """Setup the LIFX platform."""
import aiolifx import aiolifx
if sys.platform == 'win32':
_LOGGER.warning('The lifx platform is known to not work on Windows. '
'Consider using the lifx_legacy platform instead.')
server_addr = config.get(CONF_SERVER) server_addr = config.get(CONF_SERVER)
lifx_manager = LIFXManager(hass, async_add_devices) lifx_manager = LIFXManager(hass, async_add_devices)
@ -90,7 +95,7 @@ class LIFXManager(object):
entity = LIFXLight(device) entity = LIFXLight(device)
_LOGGER.debug("%s register READY", entity.ipaddr) _LOGGER.debug("%s register READY", entity.ipaddr)
self.entities[device.mac_addr] = entity self.entities[device.mac_addr] = entity
self.hass.async_add_job(self.async_add_devices, [entity]) self.async_add_devices([entity])
@callback @callback
def unregister(self, device): def unregister(self, device):

View File

@ -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]

View File

@ -25,14 +25,13 @@ CONF_BRIDGES = 'bridges'
CONF_GROUPS = 'groups' CONF_GROUPS = 'groups'
CONF_NUMBER = 'number' CONF_NUMBER = 'number'
CONF_VERSION = 'version' CONF_VERSION = 'version'
CONF_BRIDGE_LED = 'bridge_led'
DEFAULT_LED_TYPE = 'rgbw' DEFAULT_LED_TYPE = 'rgbw'
DEFAULT_PORT = 5987 DEFAULT_PORT = 5987
DEFAULT_TRANSITION = 0 DEFAULT_TRANSITION = 0
DEFAULT_VERSION = 6 DEFAULT_VERSION = 6
LED_TYPE = ['rgbw', 'rgbww', 'white'] LED_TYPE = ['rgbw', 'rgbww', 'white', 'bridge-led']
RGB_BOUNDARY = 40 RGB_BOUNDARY = 40
@ -54,7 +53,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_VERSION, vol.Optional(CONF_VERSION,
default=DEFAULT_VERSION): cv.positive_int, default=DEFAULT_VERSION): cv.positive_int,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_BRIDGE_LED, default=False): cv.boolean,
vol.Required(CONF_GROUPS): vol.All(cv.ensure_list, [ vol.Required(CONF_GROUPS): vol.All(cv.ensure_list, [
{ {
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
@ -116,8 +114,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
group_conf.get(CONF_NAME), group_conf.get(CONF_NAME),
group_conf.get(CONF_TYPE, DEFAULT_LED_TYPE)) group_conf.get(CONF_TYPE, DEFAULT_LED_TYPE))
lights.append(LimitlessLEDGroup.factory(group)) lights.append(LimitlessLEDGroup.factory(group))
if bridge_conf.get(CONF_BRIDGE_LED) and bridge.bridge_led is not None:
lights.append(LimitlessLEDGroup.factory(bridge.bridge_led))
add_devices(lights) add_devices(lights)

View File

@ -48,7 +48,7 @@ class LiteJetLight(Light):
def _on_load_changed(self): def _on_load_changed(self):
"""Called on a LiteJet thread when a load's state changes.""" """Called on a LiteJet thread when a load's state changes."""
_LOGGER.debug("Updating due to notification for %s", self._name) _LOGGER.debug("Updating due to notification for %s", self._name)
self._hass.async_add_job(self.async_update_ha_state(True)) self.schedule_update_ha_state(True)
@property @property
def supported_features(self): def supported_features(self):

View File

@ -19,7 +19,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Lutron Caseta lights.""" """Setup Lutron Caseta lights."""
devs = [] devs = []
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
light_devices = bridge.get_devices_by_type("WallDimmer") light_devices = bridge.get_devices_by_types(["WallDimmer", "PlugInDimmer"])
for light_device in light_devices: for light_device in light_devices:
dev = LutronCasetaLight(light_device, bridge) dev = LutronCasetaLight(light_device, bridge)
devs.append(dev) devs.append(dev)

View File

@ -148,16 +148,12 @@ class MqttJson(Light):
self._flash_times = flash_times self._flash_times = flash_times
self._supported_features = (SUPPORT_TRANSITION | SUPPORT_FLASH) self._supported_features = (SUPPORT_TRANSITION | SUPPORT_FLASH)
self._supported_features |= (rgb is not None and SUPPORT_RGB_COLOR) self._supported_features |= (rgb and SUPPORT_RGB_COLOR)
self._supported_features |= (brightness is not None and self._supported_features |= (brightness and SUPPORT_BRIGHTNESS)
SUPPORT_BRIGHTNESS) self._supported_features |= (color_temp and SUPPORT_COLOR_TEMP)
self._supported_features |= (color_temp is not None and self._supported_features |= (effect and SUPPORT_EFFECT)
SUPPORT_COLOR_TEMP) self._supported_features |= (white_value and SUPPORT_WHITE_VALUE)
self._supported_features |= (effect is not None and self._supported_features |= (xy and SUPPORT_XY_COLOR)
SUPPORT_EFFECT)
self._supported_features |= (white_value is not None and
SUPPORT_WHITE_VALUE)
self._supported_features |= (xy is not None and SUPPORT_XY_COLOR)
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):

View File

@ -9,15 +9,12 @@ import subprocess
import voluptuous as vol import voluptuous as vol
# Import the device class from the component that you want to support import homeassistant.helpers.config_validation as cv
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR,
ATTR_RGB_COLOR, SUPPORT_RGB_COLOR,
Light, PLATFORM_SCHEMA) Light, PLATFORM_SCHEMA)
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
# Home Assistant depends on 3rd party packages for API specific code.
REQUIREMENTS = ['piglow==1.2.4'] REQUIREMENTS = ['piglow==1.2.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Piglow Light platform.""" """Set up the Piglow Light platform."""
import piglow import piglow
if subprocess.getoutput("i2cdetect -q -y 1 | grep -o 54") != '54': if subprocess.getoutput("i2cdetect -q -y 1 | grep -o 54") != '54':
@ -41,7 +38,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
# Add devices
add_devices([PiglowLight(piglow, name)]) add_devices([PiglowLight(piglow, name)])

View File

@ -10,12 +10,14 @@ import logging
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
from homeassistant.components.rflink import ( from homeassistant.components.rflink import (
CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICE_DEFAULTS, CONF_DEVICES,
CONF_IGNORE_DEVICES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASSES, CONF_IGNORE_DEVICES,
DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA, DOMAIN, CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER,
EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, vol) DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA,
DOMAIN, EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, vol)
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_PLATFORM, CONF_TYPE, STATE_UNKNOWN) CONF_NAME, CONF_PLATFORM, CONF_TYPE, STATE_UNKNOWN)
DEPENDENCIES = ['rflink'] DEPENDENCIES = ['rflink']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,6 +32,7 @@ PLATFORM_SCHEMA = vol.Schema({
vol.Optional(CONF_IGNORE_DEVICES): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_IGNORE_DEVICES): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})): vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})):
DEVICE_DEFAULTS_SCHEMA, DEVICE_DEFAULTS_SCHEMA,
vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean,
vol.Optional(CONF_DEVICES, default={}): vol.Schema({ vol.Optional(CONF_DEVICES, default={}): vol.Schema({
cv.string: { cv.string: {
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
@ -38,8 +41,13 @@ PLATFORM_SCHEMA = vol.Schema({
TYPE_HYBRID, TYPE_TOGGLE), TYPE_HYBRID, TYPE_TOGGLE),
vol.Optional(CONF_ALIASSES, default=[]): vol.Optional(CONF_ALIASSES, default=[]):
vol.All(cv.ensure_list, [cv.string]), vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_GROUP_ALIASSES, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_NOGROUP_ALIASSES, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int), vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int),
vol.Optional(CONF_GROUP, default=True): cv.boolean,
}, },
}), }),
}) })
@ -110,7 +118,24 @@ def devices_from_config(domain_config, hass=None):
devices.append(device) devices.append(device)
# Register entity (and aliasses) to listen to incoming rflink events # Register entity (and aliasses) to listen to incoming rflink events
for _id in [device_id] + config[CONF_ALIASSES]:
# device id and normal aliasses respond to normal and group command
hass.data[DATA_ENTITY_LOOKUP][
EVENT_KEY_COMMAND][device_id].append(device)
if config[CONF_GROUP]:
hass.data[DATA_ENTITY_GROUP_LOOKUP][
EVENT_KEY_COMMAND][device_id].append(device)
for _id in config[CONF_ALIASSES]:
hass.data[DATA_ENTITY_LOOKUP][
EVENT_KEY_COMMAND][_id].append(device)
hass.data[DATA_ENTITY_GROUP_LOOKUP][
EVENT_KEY_COMMAND][_id].append(device)
# group_aliasses only respond to group commands
for _id in config[CONF_GROUP_ALIASSES]:
hass.data[DATA_ENTITY_GROUP_LOOKUP][
EVENT_KEY_COMMAND][_id].append(device)
# nogroup_aliasses only respond to normal commands
for _id in config[CONF_NOGROUP_ALIASSES]:
hass.data[DATA_ENTITY_LOOKUP][ hass.data[DATA_ENTITY_LOOKUP][
EVENT_KEY_COMMAND][_id].append(device) EVENT_KEY_COMMAND][_id].append(device)
@ -138,9 +163,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
hass.data[DATA_ENTITY_LOOKUP][ hass.data[DATA_ENTITY_LOOKUP][
EVENT_KEY_COMMAND][device_id].append(device) EVENT_KEY_COMMAND][device_id].append(device)
# Make sure the event is processed by the new entity # Schedule task to process event after entity is created
device.handle_event(event) hass.async_add_job(device.handle_event, event)
if config[CONF_AUTOMATIC_ADD]:
hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_COMMAND] = add_new_device hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_COMMAND] = add_new_device

View File

@ -8,7 +8,6 @@ import logging
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS) ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS)
from homeassistant.const import (STATE_OFF, STATE_ON)
from homeassistant.components.vera import ( from homeassistant.components.vera import (
VERA_CONTROLLER, VERA_DEVICES, VeraDevice) VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
@ -53,13 +52,13 @@ class VeraLight(VeraDevice, Light):
else: else:
self.vera_device.switch_on() self.vera_device.switch_on()
self._state = STATE_ON self._state = True
self.schedule_update_ha_state(True) self.schedule_update_ha_state(True)
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the light off.""" """Turn the light off."""
self.vera_device.switch_off() self.vera_device.switch_off()
self._state = STATE_OFF self._state = False
self.schedule_update_ha_state() self.schedule_update_ha_state()
@property @property

View File

@ -44,13 +44,14 @@ DEVICE_SCHEMA = vol.Schema({
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, }) {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, })
SUPPORT_YEELIGHT_RGB = (SUPPORT_RGB_COLOR |
SUPPORT_COLOR_TEMP)
SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS |
SUPPORT_TRANSITION | SUPPORT_TRANSITION |
SUPPORT_FLASH) SUPPORT_FLASH)
SUPPORT_YEELIGHT_RGB = (SUPPORT_YEELIGHT |
SUPPORT_RGB_COLOR |
SUPPORT_COLOR_TEMP)
def _cmd(func): def _cmd(func):
"""A wrapper to catch exceptions from the bulb.""" """A wrapper to catch exceptions from the bulb."""
@ -179,9 +180,6 @@ class YeelightLight(Light):
self._bulb_device = yeelight.Bulb(self._ipaddr) self._bulb_device = yeelight.Bulb(self._ipaddr)
self._bulb_device.get_properties() # force init for type self._bulb_device.get_properties() # force init for type
btype = self._bulb_device.bulb_type
if btype == yeelight.BulbType.Color:
self._supported_features |= SUPPORT_YEELIGHT_RGB
self._available = True self._available = True
except yeelight.BulbException as ex: except yeelight.BulbException as ex:
self._available = False self._available = False
@ -203,6 +201,9 @@ class YeelightLight(Light):
try: try:
self._bulb.get_properties() self._bulb.get_properties()
if self._bulb_device.bulb_type == yeelight.BulbType.Color:
self._supported_features = SUPPORT_YEELIGHT_RGB
self._is_on = self._properties.get("power") == "on" self._is_on = self._properties.get("power") == "on"
bright = self._properties.get("bright", None) bright = self._properties.get("bright", None)
@ -254,7 +255,8 @@ class YeelightLight(Light):
def set_flash(self, flash) -> None: def set_flash(self, flash) -> None:
"""Activate flash.""" """Activate flash."""
if flash: if flash:
from yeelight import RGBTransition, SleepTransition, Flow from yeelight import (RGBTransition, SleepTransition, Flow,
BulbException)
if self._bulb.last_properties["color_mode"] != 1: if self._bulb.last_properties["color_mode"] != 1:
_LOGGER.error("Flash supported currently only in RGB mode.") _LOGGER.error("Flash supported currently only in RGB mode.")
return return
@ -279,10 +281,14 @@ class YeelightLight(Light):
duration=duration)) duration=duration))
flow = Flow(count=count, transitions=transitions) flow = Flow(count=count, transitions=transitions)
try:
self._bulb.start_flow(flow) self._bulb.start_flow(flow)
except BulbException as ex:
_LOGGER.error("Unable to set flash: %s", ex)
def turn_on(self, **kwargs) -> None: def turn_on(self, **kwargs) -> None:
"""Turn the bulb on.""" """Turn the bulb on."""
import yeelight
brightness = kwargs.get(ATTR_BRIGHTNESS) brightness = kwargs.get(ATTR_BRIGHTNESS)
colortemp = kwargs.get(ATTR_COLOR_TEMP) colortemp = kwargs.get(ATTR_COLOR_TEMP)
rgb = kwargs.get(ATTR_RGB_COLOR) rgb = kwargs.get(ATTR_RGB_COLOR)
@ -292,22 +298,43 @@ class YeelightLight(Light):
if ATTR_TRANSITION in kwargs: # passed kwarg overrides config if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
try:
self._bulb.turn_on(duration=duration) self._bulb.turn_on(duration=duration)
except yeelight.BulbException as ex:
_LOGGER.error("Unable to turn the bulb on: %s", ex)
return
if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode: if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode:
try:
self.set_music_mode(self.config[CONF_MODE_MUSIC]) self.set_music_mode(self.config[CONF_MODE_MUSIC])
except yeelight.BulbException as ex:
_LOGGER.error("Unable to turn on music mode,"
"consider disabling it: %s", ex)
try:
# values checked for none in methods # values checked for none in methods
self.set_rgb(rgb, duration) self.set_rgb(rgb, duration)
self.set_colortemp(colortemp, duration) self.set_colortemp(colortemp, duration)
self.set_brightness(brightness, duration) self.set_brightness(brightness, duration)
self.set_flash(flash) self.set_flash(flash)
except yeelight.BulbException as ex:
_LOGGER.error("Unable to set bulb properties: %s", ex)
return
# save the current state if we had a manual change. # save the current state if we had a manual change.
if self.config[CONF_SAVE_ON_CHANGE]: if self.config[CONF_SAVE_ON_CHANGE] and (brightness
if brightness or colortemp or rgb: or colortemp
or rgb):
try:
self.set_default() self.set_default()
except yeelight.BulbException as ex:
_LOGGER.error("Unable to set the defaults: %s", ex)
return
def turn_off(self, **kwargs) -> None: def turn_off(self, **kwargs) -> None:
"""Turn off.""" """Turn off."""
import yeelight
try:
self._bulb.turn_off() self._bulb.turn_off()
except yeelight.BulbException as ex:
_LOGGER.error("Unable to turn the bulb off: %s", ex)

View File

@ -5,21 +5,19 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.yeelightsunflower/ https://home-assistant.io/components/light.yeelightsunflower/
""" """
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.light import (Light,
ATTR_RGB_COLOR, SUPPORT_RGB_COLOR,
ATTR_BRIGHTNESS,
SUPPORT_BRIGHTNESS,
PLATFORM_SCHEMA)
from homeassistant.const import CONF_HOST
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.light import (
Light, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, ATTR_BRIGHTNESS,
SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA)
from homeassistant.const import CONF_HOST
REQUIREMENTS = ['yeelightsunflower==0.0.8'] REQUIREMENTS = ['yeelightsunflower==0.0.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -35,7 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hub = yeelightsunflower.Hub(host) hub = yeelightsunflower.Hub(host)
if not hub.available: if not hub.available:
_LOGGER.error('Could not connect to Yeelight Sunflower hub') _LOGGER.error("Could not connect to Yeelight Sunflower hub")
return False return False
add_devices(SunflowerBulb(light) for light in hub.get_lights()) add_devices(SunflowerBulb(light) for light in hub.get_lights())
@ -55,7 +53,7 @@ class SunflowerBulb(Light):
@property @property
def name(self): def name(self):
"""Return the display name of this light.""" """Return the display name of this light."""
return "sunflower_{}".format(self._light.zid) return 'sunflower_{}'.format(self._light.zid)
@property @property
def available(self): def available(self):

View File

@ -10,8 +10,8 @@ import logging
# pylint: disable=import-error # pylint: disable=import-error
from threading import Timer from threading import Timer
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \ from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \ ATTR_RGB_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \
SUPPORT_RGB_COLOR, DOMAIN, Light SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, DOMAIN, Light
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
@ -43,11 +43,6 @@ TEMP_MID_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 2 + HASS_COLOR_MIN
TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN
TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN
SUPPORT_ZWAVE_DIMMER = SUPPORT_BRIGHTNESS
SUPPORT_ZWAVE_COLOR = SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
SUPPORT_ZWAVE_COLORTEMP = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
| SUPPORT_COLOR_TEMP)
def get_device(node, values, node_config, **kwargs): def get_device(node, values, node_config, **kwargs):
"""Create zwave entity device.""" """Create zwave entity device."""
@ -72,6 +67,13 @@ def brightness_state(value):
return 0, STATE_OFF return 0, STATE_OFF
def ct_to_rgb(temp):
"""Convert color temperature (mireds) to RGB."""
colorlist = list(
color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp)))
return [int(val) for val in colorlist]
class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
"""Representation of a Z-Wave dimmer.""" """Representation of a Z-Wave dimmer."""
@ -80,6 +82,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN) zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
self._brightness = None self._brightness = None
self._state = None self._state = None
self._supported_features = None
self._delay = delay self._delay = delay
self._refresh_value = refresh self._refresh_value = refresh
self._zw098 = None self._zw098 = None
@ -100,6 +103,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
self._timer = None self._timer = None
_LOGGER.debug('self._refreshing=%s self.delay=%s', _LOGGER.debug('self._refreshing=%s self.delay=%s',
self._refresh_value, self._delay) self._refresh_value, self._delay)
self.value_added()
self.update_properties() self.update_properties()
def update_properties(self): def update_properties(self):
@ -107,6 +111,12 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
# Brightness # Brightness
self._brightness, self._state = brightness_state(self.values.primary) self._brightness, self._state = brightness_state(self.values.primary)
def value_added(self):
"""Called when a new value is added to this entity."""
self._supported_features = SUPPORT_BRIGHTNESS
if self.values.dimming_duration is not None:
self._supported_features |= SUPPORT_TRANSITION
def value_changed(self): def value_changed(self):
"""Called when a value for this entity's node has changed.""" """Called when a value for this entity's node has changed."""
if self._refresh_value: if self._refresh_value:
@ -139,10 +149,43 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_ZWAVE_DIMMER return self._supported_features
def _set_duration(self, **kwargs):
"""Set the transition time for the brightness value.
Zwave Dimming Duration values:
0x00 = instant
0x01-0x7F = 1 second to 127 seconds
0x80-0xFE = 1 minute to 127 minutes
0xFF = factory default
"""
if self.values.dimming_duration is None:
if ATTR_TRANSITION in kwargs:
_LOGGER.debug("Dimming not supported by %s.", self.entity_id)
return
if ATTR_TRANSITION not in kwargs:
self.values.dimming_duration.data = 0xFF
return
transition = kwargs[ATTR_TRANSITION]
if transition <= 127:
self.values.dimming_duration.data = int(transition)
elif transition > 7620:
self.values.dimming_duration.data = 0xFE
_LOGGER.warning("Transition clipped to 127 minutes for %s.",
self.entity_id)
else:
minutes = int(transition / 60)
_LOGGER.debug("Transition rounded to %d minutes for %s.",
minutes, self.entity_id)
self.values.dimming_duration.data = minutes + 0x7F
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the device on.""" """Turn the device on."""
self._set_duration(**kwargs)
# Zwave multilevel switches use a range of [0, 99] to control # Zwave multilevel switches use a range of [0, 99] to control
# brightness. Level 255 means to set it to previous value. # brightness. Level 255 means to set it to previous value.
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
@ -156,17 +199,12 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the device off.""" """Turn the device off."""
self._set_duration(**kwargs)
if self.node.set_dimmer(self.values.primary.value_id, 0): if self.node.set_dimmer(self.values.primary.value_id, 0):
self._state = STATE_OFF self._state = STATE_OFF
def ct_to_rgb(temp):
"""Convert color temperature (mireds) to RGB."""
colorlist = list(
color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp)))
return [int(val) for val in colorlist]
class ZwaveColorLight(ZwaveDimmer): class ZwaveColorLight(ZwaveDimmer):
"""Representation of a Z-Wave color changing light.""" """Representation of a Z-Wave color changing light."""
@ -178,6 +216,14 @@ class ZwaveColorLight(ZwaveDimmer):
super().__init__(values, refresh, delay) super().__init__(values, refresh, delay)
def value_added(self):
"""Called when a new value is added to this entity."""
super().value_added()
self._supported_features |= SUPPORT_RGB_COLOR
if self._zw098:
self._supported_features |= SUPPORT_COLOR_TEMP
def update_properties(self): def update_properties(self):
"""Update internal properties based on zwave values.""" """Update internal properties based on zwave values."""
super().update_properties() super().update_properties()
@ -288,11 +334,3 @@ class ZwaveColorLight(ZwaveDimmer):
self.values.color.data = rgbw self.values.color.data = rgbw
super().turn_on(**kwargs) super().turn_on(**kwargs)
@property
def supported_features(self):
"""Flag supported features."""
if self._zw098:
return SUPPORT_ZWAVE_COLORTEMP
else:
return SUPPORT_ZWAVE_COLOR

View File

@ -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

View File

@ -53,8 +53,8 @@ LOCK_ALARM_TYPE = {
'9': 'Deadbolt Jammed', '9': 'Deadbolt Jammed',
'18': 'Locked with Keypad by user ', '18': 'Locked with Keypad by user ',
'19': 'Unlocked with Keypad by user ', '19': 'Unlocked with Keypad by user ',
'21': 'Manually Locked by ', '21': 'Manually Locked ',
'22': 'Manually Unlocked by Key or Inside thumb turn', '22': 'Manually Unlocked ',
'24': 'Locked by RF', '24': 'Locked by RF',
'25': 'Unlocked by RF', '25': 'Unlocked by RF',
'27': 'Auto re-lock', '27': 'Auto re-lock',
@ -69,8 +69,8 @@ LOCK_ALARM_TYPE = {
} }
MANUAL_LOCK_ALARM_LEVEL = { MANUAL_LOCK_ALARM_LEVEL = {
'1': 'Key Cylinder or Inside thumb turn', '1': 'by Key Cylinder or Inside thumb turn',
'2': 'Touch function (lock and leave)' '2': 'by Touch function (lock and leave)'
} }
TAMPER_ALARM_LEVEL = { TAMPER_ALARM_LEVEL = {

View File

@ -17,8 +17,8 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['https://github.com/gurumitts/' REQUIREMENTS = ['https://github.com/gurumitts/'
'pylutron-caseta/archive/v0.2.4.zip#' 'pylutron-caseta/archive/v0.2.5.zip#'
'pylutron-caseta==v0.2.4'] 'pylutron-caseta==v0.2.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -864,10 +864,10 @@ def _async_fetch_image(hass, url):
content, content_type = (None, None) content, content_type = (None, None)
websession = async_get_clientsession(hass) websession = async_get_clientsession(hass)
response = None
try: try:
with async_timeout.timeout(10, loop=hass.loop): with async_timeout.timeout(10, loop=hass.loop):
response = yield from websession.get(url) response = yield from websession.get(url)
if response.status == 200: if response.status == 200:
content = yield from response.read() content = yield from response.read()
content_type = response.headers.get(CONTENT_TYPE_HEADER) content_type = response.headers.get(CONTENT_TYPE_HEADER)
@ -875,10 +875,6 @@ def _async_fetch_image(hass, url):
except asyncio.TimeoutError: except asyncio.TimeoutError:
pass pass
finally:
if response is not None:
yield from response.release()
if not content: if not content:
return (None, None) return (None, None)

View File

@ -21,7 +21,7 @@ from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pyemby==1.1'] REQUIREMENTS = ['pyemby==1.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -42,7 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Frontier Silicon platform.""" """Set up the Frontier Silicon platform."""
import requests import requests
if discovery_info is not None: if discovery_info is not None:
@ -59,10 +59,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices( add_devices(
[FSAPIDevice(DEVICE_URL.format(host, port), password)], [FSAPIDevice(DEVICE_URL.format(host, port), password)],
update_before_add=True) update_before_add=True)
_LOGGER.debug('FSAPI device %s:%s -> %s', host, port, password) _LOGGER.debug("FSAPI device %s:%s -> %s", host, port, password)
return True return True
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
_LOGGER.error('Could not add the FSAPI device at %s:%s -> %s', _LOGGER.error("Could not add the FSAPI device at %s:%s -> %s",
host, port, password) host, port, password)
return False return False

View File

@ -27,7 +27,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.deprecation import get_deprecated from homeassistant.helpers.deprecation import get_deprecated
REQUIREMENTS = ['jsonrpc-async==0.4', 'jsonrpc-websocket==0.3'] REQUIREMENTS = ['jsonrpc-async==0.6', 'jsonrpc-websocket==0.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -14,8 +14,9 @@ from homeassistant.components.media_player import (
from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME) from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['https://github.com/danieljkemp/onkyo-eiscp/archive/' REQUIREMENTS = ['https://github.com/miracle2k/onkyo-eiscp/archive/'
'python3.zip#onkyo-eiscp==0.9.2'] '066023aec04770518d494c32fb72eea0ec5c1b7c.zip#'
'onkyo-eiscp==1.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -127,7 +128,7 @@ class OnkyoDevice(MediaPlayerDevice):
self._current_source = '_'.join( self._current_source = '_'.join(
[i for i in current_source_tuples[1]]) [i for i in current_source_tuples[1]])
self._muted = bool(mute_raw[1] == 'on') self._muted = bool(mute_raw[1] == 'on')
self._volume = int(volume_raw[1], 16) / 80.0 self._volume = volume_raw[1] / 80.0
@property @property
def name(self): def name(self):

View File

@ -4,11 +4,14 @@ Support to interface with Sonos players (via SoCo).
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.sonos/ https://home-assistant.io/components/media_player.sonos/
""" """
import asyncio
import datetime import datetime
import functools as ft
import logging import logging
from os import path from os import path
import socket import socket
import urllib import urllib
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
@ -107,7 +110,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return return
if player.is_visible: if player.is_visible:
device = SonosDevice(hass, player) device = SonosDevice(player)
add_devices([device], True) add_devices([device], True)
hass.data[DATA_SONOS].append(device) hass.data[DATA_SONOS].append(device)
if len(hass.data[DATA_SONOS]) > 1: if len(hass.data[DATA_SONOS]) > 1:
@ -132,7 +135,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.warning('No Sonos speakers found.') _LOGGER.warning('No Sonos speakers found.')
return return
hass.data[DATA_SONOS] = [SonosDevice(hass, p) for p in players] hass.data[DATA_SONOS] = [SonosDevice(p) for p in players]
add_devices(hass.data[DATA_SONOS], True) add_devices(hass.data[DATA_SONOS], True)
_LOGGER.info('Added %s Sonos speakers', len(players)) _LOGGER.info('Added %s Sonos speakers', len(players))
@ -216,19 +219,42 @@ class _ProcessSonosEventQueue():
def _get_entity_from_soco(hass, soco): def _get_entity_from_soco(hass, soco):
"""Return SonosDevice from SoCo.""" """Return SonosDevice from SoCo."""
for device in hass.data[DATA_SONOS]: for device in hass.data[DATA_SONOS]:
if soco == device.soco_device: if soco == device.soco:
return device return device
raise ValueError("No entity for SoCo device!") raise ValueError("No entity for SoCo device!")
def soco_error(funct):
"""Decorator to catch soco exceptions."""
@ft.wraps(funct)
def wrapper(*args, **kwargs):
"""Wrapper for all soco exception."""
from soco.exceptions import SoCoException
try:
return funct(*args, **kwargs)
except SoCoException as err:
_LOGGER.error("Error on %s with %s.", funct.__name__, err)
return wrapper
def soco_coordinator(funct):
"""Decorator to call funct on coordinator."""
@ft.wraps(funct)
def wrapper(device, *args, **kwargs):
"""Wrapper for call to coordinator."""
if device.is_coordinator:
return funct(device, *args, **kwargs)
return funct(device.coordinator, *args, **kwargs)
return wrapper
class SonosDevice(MediaPlayerDevice): class SonosDevice(MediaPlayerDevice):
"""Representation of a Sonos device.""" """Representation of a Sonos device."""
def __init__(self, hass, player): def __init__(self, player):
"""Initialize the Sonos device.""" """Initialize the Sonos device."""
from soco.snapshot import Snapshot
self.hass = hass
self.volume_increment = 5 self.volume_increment = 5
self._unique_id = player.uid self._unique_id = player.uid
self._player = player self._player = player
@ -260,9 +286,14 @@ class SonosDevice(MediaPlayerDevice):
self._is_playing_tv = None self._is_playing_tv = None
self._favorite_sources = None self._favorite_sources = None
self._source_name = None self._source_name = None
self.soco_snapshot = Snapshot(self._player) self._soco_snapshot = None
self._snapshot_group = None self._snapshot_group = None
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe sonos events."""
self.hass.loop.run_in_executor(None, self._subscribe_to_player_events)
@property @property
def should_poll(self): def should_poll(self):
"""Polling needed.""" """Polling needed."""
@ -297,7 +328,7 @@ class SonosDevice(MediaPlayerDevice):
return self._coordinator is None return self._coordinator is None
@property @property
def soco_device(self): def soco(self):
"""Return soco device.""" """Return soco device."""
return self._player return self._player
@ -327,7 +358,6 @@ class SonosDevice(MediaPlayerDevice):
auto_renew=True, auto_renew=True,
event_queue=self._queue) event_queue=self._queue)
# pylint: disable=too-many-branches, too-many-statements
def update(self): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
if self._speaker_info is None: if self._speaker_info is None:
@ -606,16 +636,6 @@ class SonosDevice(MediaPlayerDevice):
self._is_playing_tv = is_playing_tv self._is_playing_tv = is_playing_tv
self._is_playing_line_in = is_playing_line_in self._is_playing_line_in = is_playing_line_in
self._source_name = source_name self._source_name = source_name
# update state of the whole group
for device in [x for x in self.hass.data[DATA_SONOS]
if x.coordinator == self]:
if device.entity_id is not self.entity_id:
self.schedule_update_ha_state()
if self._queue is None and self.entity_id is not None:
self._subscribe_to_player_events()
self._last_avtransport_event = None self._last_avtransport_event = None
def _format_media_image_url(self, url, fallback_uri): def _format_media_image_url(self, url, fallback_uri):
@ -781,27 +801,31 @@ class SonosDevice(MediaPlayerDevice):
return supported return supported
@soco_error
def volume_up(self): def volume_up(self):
"""Volume up media player.""" """Volume up media player."""
self._player.volume += self.volume_increment self._player.volume += self.volume_increment
@soco_error
def volume_down(self): def volume_down(self):
"""Volume down media player.""" """Volume down media player."""
self._player.volume -= self.volume_increment self._player.volume -= self.volume_increment
@soco_error
def set_volume_level(self, volume): def set_volume_level(self, volume):
"""Set volume level, range 0..1.""" """Set volume level, range 0..1."""
self._player.volume = str(int(volume * 100)) self._player.volume = str(int(volume * 100))
@soco_error
def mute_volume(self, mute): def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player.""" """Mute (true) or unmute (false) media player."""
self._player.mute = mute self._player.mute = mute
@soco_error
@soco_coordinator
def select_source(self, source): def select_source(self, source):
"""Select input source.""" """Select input source."""
if self._coordinator: if source == SUPPORT_SOURCE_LINEIN:
self._coordinator.select_source(source)
elif source == SUPPORT_SOURCE_LINEIN:
self._source_name = SUPPORT_SOURCE_LINEIN self._source_name = SUPPORT_SOURCE_LINEIN
self._player.switch_to_line_in() self._player.switch_to_line_in()
elif source == SUPPORT_SOURCE_TV: elif source == SUPPORT_SOURCE_TV:
@ -842,72 +866,66 @@ class SonosDevice(MediaPlayerDevice):
else: else:
return self._source_name return self._source_name
@soco_error
def turn_off(self): def turn_off(self):
"""Turn off media player.""" """Turn off media player."""
self.media_pause() self.media_pause()
@soco_error
@soco_coordinator
def media_play(self): def media_play(self):
"""Send play command.""" """Send play command."""
if self._coordinator:
self._coordinator.media_play()
else:
self._player.play() self._player.play()
@soco_error
@soco_coordinator
def media_stop(self): def media_stop(self):
"""Send stop command.""" """Send stop command."""
if self._coordinator:
self._coordinator.media_stop()
else:
self._player.stop() self._player.stop()
@soco_error
@soco_coordinator
def media_pause(self): def media_pause(self):
"""Send pause command.""" """Send pause command."""
if self._coordinator:
self._coordinator.media_pause()
else:
self._player.pause() self._player.pause()
@soco_error
@soco_coordinator
def media_next_track(self): def media_next_track(self):
"""Send next track command.""" """Send next track command."""
if self._coordinator:
self._coordinator.media_next_track()
else:
self._player.next() self._player.next()
@soco_error
@soco_coordinator
def media_previous_track(self): def media_previous_track(self):
"""Send next track command.""" """Send next track command."""
if self._coordinator:
self._coordinator.media_previous_track()
else:
self._player.previous() self._player.previous()
@soco_error
@soco_coordinator
def media_seek(self, position): def media_seek(self, position):
"""Send seek command.""" """Send seek command."""
if self._coordinator:
self._coordinator.media_seek(position)
else:
self._player.seek(str(datetime.timedelta(seconds=int(position)))) self._player.seek(str(datetime.timedelta(seconds=int(position))))
@soco_error
@soco_coordinator
def clear_playlist(self): def clear_playlist(self):
"""Clear players playlist.""" """Clear players playlist."""
if self._coordinator:
self._coordinator.clear_playlist()
else:
self._player.clear_queue() self._player.clear_queue()
@soco_error
def turn_on(self): def turn_on(self):
"""Turn the media player on.""" """Turn the media player on."""
self.media_play() self.media_play()
@soco_error
@soco_coordinator
def play_media(self, media_type, media_id, **kwargs): def play_media(self, media_type, media_id, **kwargs):
""" """
Send the play_media command to the media player. Send the play_media command to the media player.
If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
""" """
if self._coordinator:
self._coordinator.play_media(media_type, media_id, **kwargs)
else:
if kwargs.get(ATTR_MEDIA_ENQUEUE): if kwargs.get(ATTR_MEDIA_ENQUEUE):
from soco.exceptions import SoCoUPnPException from soco.exceptions import SoCoUPnPException
try: try:
@ -919,6 +937,7 @@ class SonosDevice(MediaPlayerDevice):
else: else:
self._player.play_uri(media_id) self._player.play_uri(media_id)
@soco_error
def join(self, master): def join(self, master):
"""Join the player to a group.""" """Join the player to a group."""
coord = [device for device in self.hass.data[DATA_SONOS] coord = [device for device in self.hass.data[DATA_SONOS]
@ -926,29 +945,26 @@ class SonosDevice(MediaPlayerDevice):
if coord and master != self.entity_id: if coord and master != self.entity_id:
coord = coord[0] coord = coord[0]
if coord.soco_device.group.coordinator != coord.soco_device: if coord.soco.group.coordinator != coord.soco:
coord.soco_device.unjoin() coord.soco.unjoin()
self._player.join(coord.soco_device) self._player.join(coord.soco)
self._coordinator = coord self._coordinator = coord
else: else:
_LOGGER.error("Master not found %s", master) _LOGGER.error("Master not found %s", master)
@soco_error
def unjoin(self): def unjoin(self):
"""Unjoin the player from a group.""" """Unjoin the player from a group."""
self._player.unjoin() self._player.unjoin()
self._coordinator = None self._coordinator = None
@soco_error
def snapshot(self, with_group=True): def snapshot(self, with_group=True):
"""Snapshot the player.""" """Snapshot the player."""
from soco.exceptions import SoCoException from soco.snapshot import Snapshot
try:
self.soco_snapshot.is_playing_queue = False self._soco_snapshot = Snapshot(self._player)
self.soco_snapshot.is_coordinator = False self._soco_snapshot.snapshot()
self.soco_snapshot.snapshot()
except SoCoException:
_LOGGER.debug("Error on snapshot %s", self.entity_id)
self._snapshot_group = None
return
if with_group: if with_group:
self._snapshot_group = self._player.group self._snapshot_group = self._player.group
@ -957,14 +973,15 @@ class SonosDevice(MediaPlayerDevice):
else: else:
self._snapshot_group = None self._snapshot_group = None
@soco_error
def restore(self, with_group=True): def restore(self, with_group=True):
"""Restore snapshot for the player.""" """Restore snapshot for the player."""
from soco.exceptions import SoCoException from soco.exceptions import SoCoException
try: try:
# need catch exception if a coordinator is going to slave. # need catch exception if a coordinator is going to slave.
# this state will recover with group part. # this state will recover with group part.
self.soco_snapshot.restore(False) self._soco_snapshot.restore(False)
except (TypeError, SoCoException): except (TypeError, AttributeError, SoCoException):
_LOGGER.debug("Error on restore %s", self.entity_id) _LOGGER.debug("Error on restore %s", self.entity_id)
# restore groups # restore groups
@ -1006,18 +1023,16 @@ class SonosDevice(MediaPlayerDevice):
if s_dev != old.coordinator: if s_dev != old.coordinator:
s_dev.join(old.coordinator) s_dev.join(old.coordinator)
@soco_error
@soco_coordinator
def set_sleep_timer(self, sleep_time): def set_sleep_timer(self, sleep_time):
"""Set the timer on the player.""" """Set the timer on the player."""
if self._coordinator:
self._coordinator.set_sleep_timer(sleep_time)
else:
self._player.set_sleep_timer(sleep_time) self._player.set_sleep_timer(sleep_time)
@soco_error
@soco_coordinator
def clear_sleep_timer(self): def clear_sleep_timer(self):
"""Clear the timer on the player.""" """Clear the timer on the player."""
if self._coordinator:
self._coordinator.set_sleep_timer(None)
else:
self._player.set_sleep_timer(None) self._player.set_sleep_timer(None)
@property @property

View File

@ -117,7 +117,6 @@ class LogitechMediaServer(object):
@asyncio.coroutine @asyncio.coroutine
def async_query(self, *command, player=""): def async_query(self, *command, player=""):
"""Abstract out the JSON-RPC connection.""" """Abstract out the JSON-RPC connection."""
response = None
auth = None if self._username is None else aiohttp.BasicAuth( auth = None if self._username is None else aiohttp.BasicAuth(
self._username, self._password) self._username, self._password)
url = "http://{}:{}/jsonrpc.js".format( url = "http://{}:{}/jsonrpc.js".format(
@ -138,22 +137,17 @@ class LogitechMediaServer(object):
data=data, data=data,
auth=auth) auth=auth)
if response.status == 200: if response.status != 200:
data = yield from response.json()
else:
_LOGGER.error( _LOGGER.error(
"Query failed, response code: %s Full message: %s", "Query failed, response code: %s Full message: %s",
response.status, response) response.status, response)
return False return False
except (asyncio.TimeoutError, data = yield from response.json()
aiohttp.errors.ClientError,
aiohttp.errors.ClientDisconnectedError) as error: except (asyncio.TimeoutError, aiohttp.ClientError) as error:
_LOGGER.error("Failed communicating with LMS: %s", type(error)) _LOGGER.error("Failed communicating with LMS: %s", type(error))
return False return False
finally:
if response is not None:
yield from response.release()
try: try:
return data['result'] return data['result']

View File

@ -425,12 +425,12 @@ class UniversalMediaPlayer(MediaPlayerDevice):
return self._async_call_service( return self._async_call_service(
SERVICE_VOLUME_MUTE, data, allow_override=True) SERVICE_VOLUME_MUTE, data, allow_override=True)
def async_set_volume_level(self, volume_level): def async_set_volume_level(self, volume):
"""Set volume level, range 0..1. """Set volume level, range 0..1.
This method must be run in the event loop and returns a coroutine. This method must be run in the event loop and returns a coroutine.
""" """
data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level} data = {ATTR_MEDIA_VOLUME_LEVEL: volume}
return self._async_call_service( return self._async_call_service(
SERVICE_VOLUME_SET, data, allow_override=True) SERVICE_VOLUME_SET, data, allow_override=True)

Some files were not shown because too many files have changed in this diff Show More