Merge pull request #19897 from home-assistant/rc

0.85.0
This commit is contained in:
Paulus Schoutsen 2019-01-09 16:51:11 -08:00 committed by GitHub
commit 70a8cac19d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
446 changed files with 14582 additions and 4014 deletions

View File

@ -73,7 +73,8 @@ omit =
homeassistant/components/comfoconnect.py homeassistant/components/comfoconnect.py
homeassistant/components/*/comfoconnect.py homeassistant/components/*/comfoconnect.py
homeassistant/components/daikin.py homeassistant/components/daikin/__init__.py
homeassistant/components/daikin/const.py
homeassistant/components/*/daikin.py homeassistant/components/*/daikin.py
homeassistant/components/digital_ocean.py homeassistant/components/digital_ocean.py
@ -105,18 +106,24 @@ omit =
homeassistant/components/enocean.py homeassistant/components/enocean.py
homeassistant/components/*/enocean.py homeassistant/components/*/enocean.py
homeassistant/components/envisalink.py homeassistant/components/envisalink/__init__.py
homeassistant/components/*/envisalink.py homeassistant/components/*/envisalink.py
homeassistant/components/evohome.py homeassistant/components/evohome.py
homeassistant/components/*/evohome.py homeassistant/components/*/evohome.py
homeassistant/components/freebox.py
homeassistant/components/*/freebox.py
homeassistant/components/fritzbox.py homeassistant/components/fritzbox.py
homeassistant/components/*/fritzbox.py homeassistant/components/*/fritzbox.py
homeassistant/components/ecovacs.py homeassistant/components/ecovacs.py
homeassistant/components/*/ecovacs.py homeassistant/components/*/ecovacs.py
homeassistant/components/esphome/__init__.py
homeassistant/components/*/esphome.py
homeassistant/components/eufy.py homeassistant/components/eufy.py
homeassistant/components/*/eufy.py homeassistant/components/*/eufy.py
@ -160,6 +167,9 @@ omit =
homeassistant/components/homematicip_cloud.py homeassistant/components/homematicip_cloud.py
homeassistant/components/*/homematicip_cloud.py homeassistant/components/*/homematicip_cloud.py
homeassistant/components/homeworks.py
homeassistant/components/*/homeworks.py
homeassistant/components/huawei_lte.py homeassistant/components/huawei_lte.py
homeassistant/components/*/huawei_lte.py homeassistant/components/*/huawei_lte.py
@ -203,6 +213,9 @@ omit =
homeassistant/components/lametric.py homeassistant/components/lametric.py
homeassistant/components/*/lametric.py homeassistant/components/*/lametric.py
homeassistant/components/lcn.py
homeassistant/components/*/lcn.py
homeassistant/components/linode.py homeassistant/components/linode.py
homeassistant/components/*/linode.py homeassistant/components/*/linode.py
@ -265,6 +278,9 @@ omit =
homeassistant/components/openuv/__init__.py homeassistant/components/openuv/__init__.py
homeassistant/components/*/openuv.py homeassistant/components/*/openuv.py
homeassistant/components/plum_lightpad.py
homeassistant/components/*/plum_lightpad.py
homeassistant/components/pilight.py homeassistant/components/pilight.py
homeassistant/components/*/pilight.py homeassistant/components/*/pilight.py
@ -287,6 +303,8 @@ omit =
homeassistant/components/raspihats.py homeassistant/components/raspihats.py
homeassistant/components/*/raspihats.py homeassistant/components/*/raspihats.py
homeassistant/components/*/raspyrfm.py
homeassistant/components/rfxtrx.py homeassistant/components/rfxtrx.py
homeassistant/components/*/rfxtrx.py homeassistant/components/*/rfxtrx.py
@ -407,6 +425,7 @@ omit =
homeassistant/components/zha/__init__.py homeassistant/components/zha/__init__.py
homeassistant/components/zha/const.py homeassistant/components/zha/const.py
homeassistant/components/zha/event.py
homeassistant/components/zha/entities/* homeassistant/components/zha/entities/*
homeassistant/components/zha/helpers.py homeassistant/components/zha/helpers.py
homeassistant/components/*/zha.py homeassistant/components/*/zha.py
@ -423,6 +442,7 @@ omit =
homeassistant/components/spider.py homeassistant/components/spider.py
homeassistant/components/*/spider.py homeassistant/components/*/spider.py
homeassistant/components/air_quality/opensensemap.py
homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/canary.py
homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/concord232.py
@ -496,7 +516,6 @@ omit =
homeassistant/components/device_tracker/bt_smarthub.py homeassistant/components/device_tracker/bt_smarthub.py
homeassistant/components/device_tracker/cisco_ios.py homeassistant/components/device_tracker/cisco_ios.py
homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/freebox.py
homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/google_maps.py homeassistant/components/device_tracker/google_maps.py
homeassistant/components/device_tracker/googlehome.py homeassistant/components/device_tracker/googlehome.py
@ -533,6 +552,7 @@ omit =
homeassistant/components/folder_watcher.py homeassistant/components/folder_watcher.py
homeassistant/components/foursquare.py homeassistant/components/foursquare.py
homeassistant/components/goalfeed.py homeassistant/components/goalfeed.py
homeassistant/components/idteck_prox.py
homeassistant/components/ifttt.py homeassistant/components/ifttt.py
homeassistant/components/image_processing/dlib_face_detect.py homeassistant/components/image_processing/dlib_face_detect.py
homeassistant/components/image_processing/dlib_face_identify.py homeassistant/components/image_processing/dlib_face_identify.py
@ -596,6 +616,7 @@ omit =
homeassistant/components/media_player/frontier_silicon.py homeassistant/components/media_player/frontier_silicon.py
homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/gstreamer.py homeassistant/components/media_player/gstreamer.py
homeassistant/components/media_player/harman_kardon_avr.py
homeassistant/components/media_player/horizon.py homeassistant/components/media_player/horizon.py
homeassistant/components/media_player/itunes.py homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py homeassistant/components/media_player/kodi.py
@ -680,8 +701,10 @@ omit =
homeassistant/components/route53.py homeassistant/components/route53.py
homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/scene/lifx_cloud.py homeassistant/components/scene/lifx_cloud.py
homeassistant/components/sensor/aftership.py
homeassistant/components/sensor/airvisual.py homeassistant/components/sensor/airvisual.py
homeassistant/components/sensor/alpha_vantage.py homeassistant/components/sensor/alpha_vantage.py
homeassistant/components/sensor/ambient_station.py
homeassistant/components/sensor/arest.py homeassistant/components/sensor/arest.py
homeassistant/components/sensor/arwn.py homeassistant/components/sensor/arwn.py
homeassistant/components/sensor/bbox.py homeassistant/components/sensor/bbox.py
@ -692,6 +715,7 @@ omit =
homeassistant/components/sensor/bme680.py homeassistant/components/sensor/bme680.py
homeassistant/components/sensor/bom.py homeassistant/components/sensor/bom.py
homeassistant/components/sensor/broadlink.py homeassistant/components/sensor/broadlink.py
homeassistant/components/sensor/brottsplatskartan.py
homeassistant/components/sensor/buienradar.py homeassistant/components/sensor/buienradar.py
homeassistant/components/sensor/cert_expiry.py homeassistant/components/sensor/cert_expiry.py
homeassistant/components/sensor/citybikes.py homeassistant/components/sensor/citybikes.py
@ -737,6 +761,7 @@ omit =
homeassistant/components/sensor/google_travel_time.py homeassistant/components/sensor/google_travel_time.py
homeassistant/components/sensor/gpsd.py homeassistant/components/sensor/gpsd.py
homeassistant/components/sensor/gtfs.py homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/gtt.py
homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/htu21d.py homeassistant/components/sensor/htu21d.py
@ -752,6 +777,7 @@ omit =
homeassistant/components/sensor/launch_library.py homeassistant/components/sensor/launch_library.py
homeassistant/components/sensor/linky.py homeassistant/components/sensor/linky.py
homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/linux_battery.py
homeassistant/components/sensor/london_underground.py
homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/luftdaten.py homeassistant/components/sensor/luftdaten.py
homeassistant/components/sensor/lyft.py homeassistant/components/sensor/lyft.py
@ -769,6 +795,7 @@ omit =
homeassistant/components/sensor/netdata.py homeassistant/components/sensor/netdata.py
homeassistant/components/sensor/netdata_public.py homeassistant/components/sensor/netdata_public.py
homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/nmbs.py
homeassistant/components/sensor/noaa_tides.py homeassistant/components/sensor/noaa_tides.py
homeassistant/components/sensor/nsw_fuel_station.py homeassistant/components/sensor/nsw_fuel_station.py
homeassistant/components/sensor/nut.py homeassistant/components/sensor/nut.py
@ -785,6 +812,7 @@ omit =
homeassistant/components/sensor/pocketcasts.py homeassistant/components/sensor/pocketcasts.py
homeassistant/components/sensor/pollen.py homeassistant/components/sensor/pollen.py
homeassistant/components/sensor/postnl.py homeassistant/components/sensor/postnl.py
homeassistant/components/sensor/prezzibenzina.py
homeassistant/components/sensor/pushbullet.py homeassistant/components/sensor/pushbullet.py
homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/pvoutput.py
homeassistant/components/sensor/pyload.py homeassistant/components/sensor/pyload.py
@ -809,6 +837,7 @@ omit =
homeassistant/components/sensor/snmp.py homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/sochain.py homeassistant/components/sensor/sochain.py
homeassistant/components/sensor/socialblade.py homeassistant/components/sensor/socialblade.py
homeassistant/components/sensor/solaredge.py
homeassistant/components/sensor/sonarr.py homeassistant/components/sensor/sonarr.py
homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/spotcrime.py homeassistant/components/sensor/spotcrime.py
@ -864,6 +893,7 @@ omit =
homeassistant/components/switch/mystrom.py homeassistant/components/switch/mystrom.py
homeassistant/components/switch/netio.py homeassistant/components/switch/netio.py
homeassistant/components/switch/orvibo.py homeassistant/components/switch/orvibo.py
homeassistant/components/switch/pencom.py
homeassistant/components/switch/pulseaudio_loopback.py homeassistant/components/switch/pulseaudio_loopback.py
homeassistant/components/switch/rainbird.py homeassistant/components/switch/rainbird.py
homeassistant/components/switch/rest.py homeassistant/components/switch/rest.py

View File

@ -1,6 +1,7 @@
<!-- READ THIS FIRST: <!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/ - If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases - Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
- Frontend issues should be submitted to the home-assistant-polymer repository: https://github.com/home-assistant/home-assistant-polymer/issues
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components - Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests - This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template! - Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!

View File

@ -7,6 +7,7 @@ about: Create a report to help us improve
<!-- READ THIS FIRST: <!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/ - If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases - Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
- Frontend issues should be submitted to the home-assistant-polymer repository: https://github.com/home-assistant/home-assistant-polymer/issues
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components - Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests - This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template! - Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!

View File

@ -184,6 +184,8 @@ homeassistant/components/*/edp_redy.py @abmantis
homeassistant/components/edp_redy.py @abmantis homeassistant/components/edp_redy.py @abmantis
homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/eight_sleep.py @mezz64
homeassistant/components/*/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64
homeassistant/components/esphome/*.py @OttoWinter
homeassistant/components/*/esphome.py @OttoWinter
# H # H
homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/hive.py @Rendili @KJonline
@ -211,6 +213,10 @@ homeassistant/components/melissa.py @kennedyshead
homeassistant/components/*/melissa.py @kennedyshead homeassistant/components/*/melissa.py @kennedyshead
homeassistant/components/*/mystrom.py @fabaff homeassistant/components/*/mystrom.py @fabaff
# N
homeassistant/components/ness_alarm.py @nickw444
homeassistant/components/*/ness_alarm.py @nickw444
# O # O
homeassistant/components/openuv/* @bachya homeassistant/components/openuv/* @bachya
homeassistant/components/*/openuv.py @bachya homeassistant/components/*/openuv.py @bachya

View File

@ -1,4 +1,4 @@
Home Assistant |Build Status| |Coverage Status| |Chat Status| |Reviewed by Hound| Home Assistant |Build Status| |Coverage Status| |Chat Status|
================================================================================= =================================================================================
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. 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.
@ -33,8 +33,6 @@ of a component, check the `Home Assistant help section <https://home-assistant.i
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master :target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg .. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
:target: https://discord.gg/c5DvZ4e :target: https://discord.gg/c5DvZ4e
.. |Reviewed by Hound| image:: https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg
:target: https://houndci.com
.. |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 .. |screenshot-components| image:: https://raw.github.com/home-assistant/home-assistant/dev/docs/screenshot-components.png

View File

@ -1,5 +1,6 @@
"""Permission constants.""" """Permission constants."""
CAT_ENTITIES = 'entities' CAT_ENTITIES = 'entities'
CAT_CONFIG_ENTRIES = 'config_entries'
SUBCAT_ALL = 'all' SUBCAT_ALL = 'all'
POLICY_READ = 'read' POLICY_READ = 'read'

View File

@ -125,16 +125,23 @@ class AdsHub:
def shutdown(self, *args, **kwargs): def shutdown(self, *args, **kwargs):
"""Shutdown ADS connection.""" """Shutdown ADS connection."""
import pyads
_LOGGER.debug("Shutting down ADS") _LOGGER.debug("Shutting down ADS")
for notification_item in self._notification_items.values(): for notification_item in self._notification_items.values():
self._client.del_device_notification(
notification_item.hnotify,
notification_item.huser
)
_LOGGER.debug( _LOGGER.debug(
"Deleting device notification %d, %d", "Deleting device notification %d, %d",
notification_item.hnotify, notification_item.huser) notification_item.hnotify, notification_item.huser)
self._client.close() try:
self._client.del_device_notification(
notification_item.hnotify,
notification_item.huser
)
except pyads.ADSError as err:
_LOGGER.error(err)
try:
self._client.close()
except pyads.ADSError as err:
_LOGGER.error(err)
def register_device(self, device): def register_device(self, device):
"""Register a new device.""" """Register a new device."""

View File

@ -0,0 +1,147 @@
"""
Component for handling Air Quality data for your location.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/air_quality/
"""
from datetime import timedelta
import logging
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
ATTR_AQI = 'air_quality_index'
ATTR_ATTRIBUTION = 'attribution'
ATTR_C02 = 'carbon_dioxide'
ATTR_CO = 'carbon_monoxide'
ATTR_N2O = 'nitrogen_oxide'
ATTR_NO = 'nitrogen_monoxide'
ATTR_NO2 = 'nitrogen_dioxide'
ATTR_OZONE = 'ozone'
ATTR_PM_0_1 = 'particulate_matter_0_1'
ATTR_PM_10 = 'particulate_matter_10'
ATTR_PM_2_5 = 'particulate_matter_2_5'
ATTR_SO2 = 'sulphur_dioxide'
DOMAIN = 'air_quality'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SCAN_INTERVAL = timedelta(seconds=30)
PROP_TO_ATTR = {
'air_quality_index': ATTR_AQI,
'attribution': ATTR_ATTRIBUTION,
'carbon_dioxide': ATTR_C02,
'carbon_monoxide': ATTR_CO,
'nitrogen_oxide': ATTR_N2O,
'nitrogen_monoxide': ATTR_NO,
'nitrogen_dioxide': ATTR_NO2,
'ozone': ATTR_OZONE,
'particulate_matter_0_1': ATTR_PM_0_1,
'particulate_matter_10': ATTR_PM_10,
'particulate_matter_2_5': ATTR_PM_2_5,
'sulphur_dioxide': ATTR_SO2,
}
async def async_setup(hass, config):
"""Set up the air quality component."""
component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
await component.async_setup(config)
return True
async def async_setup_entry(hass, entry):
"""Set up a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
class AirQualityEntity(Entity):
"""ABC for air quality data."""
@property
def particulate_matter_2_5(self):
"""Return the particulate matter 2.5 level."""
raise NotImplementedError()
@property
def particulate_matter_10(self):
"""Return the particulate matter 10 level."""
return None
@property
def particulate_matter_0_1(self):
"""Return the particulate matter 0.1 level."""
return None
@property
def air_quality_index(self):
"""Return the Air Quality Index (AQI)."""
return None
@property
def ozone(self):
"""Return the O3 (ozone) level."""
return None
@property
def carbon_monoxide(self):
"""Return the CO (carbon monoxide) level."""
return None
@property
def carbon_dioxide(self):
"""Return the CO2 (carbon dioxide) level."""
return None
@property
def attribution(self):
"""Return the attribution."""
return None
@property
def sulphur_dioxide(self):
"""Return the SO2 (sulphur dioxide) level."""
return None
@property
def nitrogen_oxide(self):
"""Return the N2O (nitrogen oxide) level."""
return None
@property
def nitrogen_monoxide(self):
"""Return the NO (nitrogen monoxide) level."""
return None
@property
def nitrogen_dioxide(self):
"""Return the NO2 (nitrogen dioxide) level."""
return None
@property
def state_attributes(self):
"""Return the state attributes."""
data = {}
for prop, attr in PROP_TO_ATTR.items():
value = getattr(self, prop)
if value is not None:
data[attr] = value
return data
@property
def state(self):
"""Return the current state."""
return self.particulate_matter_2_5

View File

@ -0,0 +1,56 @@
"""
Demo platform that offers fake air quality data.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.air_quality import AirQualityEntity
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Air Quality."""
add_entities([
DemoAirQuality('Home', 14, 23, 100),
DemoAirQuality('Office', 4, 16, None)
])
class DemoAirQuality(AirQualityEntity):
"""Representation of Air Quality data."""
def __init__(self, name, pm_2_5, pm_10, n2o):
"""Initialize the Demo Air Quality."""
self._name = name
self._pm_2_5 = pm_2_5
self._pm_10 = pm_10
self._n2o = n2o
@property
def name(self):
"""Return the name of the sensor."""
return '{} {}'.format('Demo Air Quality', self._name)
@property
def should_poll(self):
"""No polling needed for Demo Air Quality."""
return False
@property
def particulate_matter_2_5(self):
"""Return the particulate matter 2.5 level."""
return self._pm_2_5
@property
def particulate_matter_10(self):
"""Return the particulate matter 10 level."""
return self._pm_10
@property
def nitrogen_oxide(self):
"""Return the nitrogen oxide (N2O) level."""
return self._n2o
@property
def attribution(self):
"""Return the attribution."""
return 'Powered by Home Assistant'

View File

@ -0,0 +1,105 @@
"""
Support for openSenseMap Air Quality data.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/air_quality/opensensemap/
"""
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.components.air_quality import (
PLATFORM_SCHEMA, AirQualityEntity)
from homeassistant.const import CONF_NAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
REQUIREMENTS = ['opensensemap-api==0.1.3']
_LOGGER = logging.getLogger(__name__)
ATTRIBUTION = 'Data provided by openSenseMap'
CONF_STATION_ID = 'station_id'
SCAN_INTERVAL = timedelta(minutes=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_STATION_ID): cv.string,
vol.Optional(CONF_NAME): cv.string,
})
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Set up the openSenseMap air quality platform."""
from opensensemap_api import OpenSenseMap
name = config.get(CONF_NAME)
station_id = config[CONF_STATION_ID]
session = async_get_clientsession(hass)
osm_api = OpenSenseMapData(OpenSenseMap(station_id, hass.loop, session))
await osm_api.async_update()
if 'name' not in osm_api.api.data:
_LOGGER.error("Station %s is not available", station_id)
return
station_name = osm_api.api.data['name'] if name is None else name
async_add_entities([OpenSenseMapQuality(station_name, osm_api)], True)
class OpenSenseMapQuality(AirQualityEntity):
"""Implementation of an openSenseMap air quality entity."""
def __init__(self, name, osm):
"""Initialize the air quality entity."""
self._name = name
self._osm = osm
@property
def name(self):
"""Return the name of the air quality entity."""
return self._name
@property
def particulate_matter_2_5(self):
"""Return the particulate matter 2.5 level."""
return self._osm.api.pm2_5
@property
def particulate_matter_10(self):
"""Return the particulate matter 10 level."""
return self._osm.api.pm10
@property
def attribution(self):
"""Return the attribution."""
return ATTRIBUTION
async def async_update(self):
"""Get the latest data from the openSenseMap API."""
await self._osm.async_update()
class OpenSenseMapData:
"""Get the latest data and update the states."""
def __init__(self, api):
"""Initialize the data object."""
self.api = api
@Throttle(SCAN_INTERVAL)
async def async_update(self):
"""Get the latest data from the Pi-hole."""
from opensensemap_api.exceptions import OpenSenseMapError
try:
await self.api.get_data()
except OpenSenseMapError as err:
_LOGGER.error("Unable to fetch data: %s", err)

View File

@ -25,7 +25,7 @@ ATTR_CHANGED_BY = 'changed_by'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
ALARM_SERVICE_SCHEMA = vol.Schema({ ALARM_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
vol.Optional(ATTR_CODE): cv.string, vol.Optional(ATTR_CODE): cv.string,
}) })

View File

@ -5,14 +5,16 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.ialarm/ https://home-assistant.io/components/alarm_control_panel.ialarm/
""" """
import logging import logging
import re
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 (
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, CONF_CODE, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyialarm==0.3'] REQUIREMENTS = ['pyialarm==0.3']
@ -36,6 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol), vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol),
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_CODE): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}) })
@ -43,23 +46,25 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up an iAlarm control panel.""" """Set up an iAlarm control panel."""
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
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)
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
url = 'http://{}'.format(host) url = 'http://{}'.format(host)
ialarm = IAlarmPanel(name, username, password, url) ialarm = IAlarmPanel(name, code, username, password, url)
add_entities([ialarm], True) add_entities([ialarm], True)
class IAlarmPanel(alarm.AlarmControlPanel): class IAlarmPanel(alarm.AlarmControlPanel):
"""Representation of an iAlarm status.""" """Representation of an iAlarm status."""
def __init__(self, name, username, password, url): def __init__(self, name, code, username, password, url):
"""Initialize the iAlarm status.""" """Initialize the iAlarm status."""
from pyialarm import IAlarm from pyialarm import IAlarm
self._name = name self._name = name
self._code = str(code) if code else None
self._username = username self._username = username
self._password = password self._password = password
self._url = url self._url = url
@ -71,6 +76,15 @@ class IAlarmPanel(alarm.AlarmControlPanel):
"""Return the name of the device.""" """Return the name of the device."""
return self._name return self._name
@property
def code_format(self):
"""Return one or more digits/characters."""
if self._code is None:
return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
@ -98,12 +112,22 @@ class IAlarmPanel(alarm.AlarmControlPanel):
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
self._client.disarm() if self._validate_code(code):
self._client.disarm()
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
self._client.arm_away() if self._validate_code(code):
self._client.arm_away()
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
self._client.arm_stay() if self._validate_code(code):
self._client.arm_stay()
def _validate_code(self, code):
"""Validate given code."""
check = self._code is None or code == self._code
if not check:
_LOGGER.warning("Wrong code entered")
return check

View File

@ -13,13 +13,14 @@ from homeassistant.core import callback
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, CONF_CODE, CONF_DEVICE, CONF_NAME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_PENDING,
CONF_NAME, CONF_CODE) STATE_ALARM_TRIGGERED, STATE_UNKNOWN)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC,
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN,
CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate, subscription) CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate,
MqttEntityDeviceInfo, subscription)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -30,6 +31,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_PAYLOAD_DISARM = 'payload_disarm' CONF_PAYLOAD_DISARM = 'payload_disarm'
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home' CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away' CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
CONF_UNIQUE_ID = 'unique_id'
DEFAULT_ARM_AWAY = 'ARM_AWAY' DEFAULT_ARM_AWAY = 'ARM_AWAY'
DEFAULT_ARM_HOME = 'ARM_HOME' DEFAULT_ARM_HOME = 'ARM_HOME'
@ -45,6 +47,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@ -73,7 +77,7 @@ async def _async_setup_entity(config, async_add_entities,
async_add_entities([MqttAlarm(config, discovery_hash)]) async_add_entities([MqttAlarm(config, discovery_hash)])
class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
alarm.AlarmControlPanel): alarm.AlarmControlPanel):
"""Representation of a MQTT alarm status.""" """Representation of a MQTT alarm status."""
@ -81,17 +85,20 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
"""Init the MQTT Alarm Control Panel.""" """Init the MQTT Alarm Control Panel."""
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._config = config self._config = config
self._unique_id = config.get(CONF_UNIQUE_ID)
self._sub_state = None self._sub_state = None
availability_topic = config.get(CONF_AVAILABILITY_TOPIC) availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
payload_available = config.get(CONF_PAYLOAD_AVAILABLE) payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
qos = config.get(CONF_QOS) qos = config.get(CONF_QOS)
device_config = config.get(CONF_DEVICE)
MqttAvailability.__init__(self, availability_topic, qos, MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available) payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash, MqttDiscoveryUpdate.__init__(self, discovery_hash,
self.discovery_update) self.discovery_update)
MqttEntityDeviceInfo.__init__(self, device_config)
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe mqtt events.""" """Subscribe mqtt events."""
@ -127,7 +134,8 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self):
"""Unsubscribe when removed.""" """Unsubscribe when removed."""
await subscription.async_unsubscribe_topics(self.hass, self._sub_state) self._sub_state = await subscription.async_unsubscribe_topics(
self.hass, self._sub_state)
await MqttAvailability.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self)
@property @property
@ -140,6 +148,11 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
"""Return the name of the device.""" """Return the name of the device."""
return self._config.get(CONF_NAME) return self._config.get(CONF_NAME)
@property
def unique_id(self):
"""Return a unique ID."""
return self._unique_id
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""

View File

@ -0,0 +1,107 @@
"""
Support for Ness D8X/D16X alarm panel.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.ness_alarm/
"""
import logging
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.ness_alarm import (
DATA_NESS, SIGNAL_ARMING_STATE_CHANGED)
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMING,
STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, STATE_ALARM_DISARMED)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['ness_alarm']
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Ness Alarm alarm control panel devices."""
if discovery_info is None:
return
device = NessAlarmPanel(hass.data[DATA_NESS], 'Alarm Panel')
async_add_entities([device])
class NessAlarmPanel(alarm.AlarmControlPanel):
"""Representation of a Ness alarm panel."""
def __init__(self, client, name):
"""Initialize the alarm panel."""
self._client = client
self._name = name
self._state = None
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_ARMING_STATE_CHANGED,
self._handle_arming_state_change)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def should_poll(self):
"""Return the polling state."""
return False
@property
def code_format(self):
"""Return the regex for code format or None if no code is required."""
return 'Number'
@property
def state(self):
"""Return the state of the device."""
return self._state
async def async_alarm_disarm(self, code=None):
"""Send disarm command."""
await self._client.disarm(code)
async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
await self._client.arm_away(code)
async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
await self._client.arm_home(code)
async def async_alarm_trigger(self, code=None):
"""Send trigger/panic command."""
await self._client.panic(code)
@callback
def _handle_arming_state_change(self, arming_state):
"""Handle arming state update."""
from nessclient import ArmingState
if arming_state == ArmingState.UNKNOWN:
self._state = None
elif arming_state == ArmingState.DISARMED:
self._state = STATE_ALARM_DISARMED
elif arming_state == ArmingState.ARMING:
self._state = STATE_ALARM_ARMING
elif arming_state == ArmingState.EXIT_DELAY:
self._state = STATE_ALARM_ARMING
elif arming_state == ArmingState.ARMED:
self._state = STATE_ALARM_ARMED_AWAY
elif arming_state == ArmingState.ENTRY_DELAY:
self._state = STATE_ALARM_PENDING
elif arming_state == ArmingState.TRIGGERED:
self._state = STATE_ALARM_TRIGGERED
else:
_LOGGER.warning("Unhandled arming state: %s", arming_state)
self.async_schedule_update_ha_state()

View File

@ -13,7 +13,7 @@ import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY, CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pynx584==0.4'] REQUIREMENTS = ['pynx584==0.4']
@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([NX584Alarm(hass, url, name)]) add_entities([NX584Alarm(hass, url, name)])
except requests.exceptions.ConnectionError as ex: except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to NX584: %s", str(ex)) _LOGGER.error("Unable to connect to NX584: %s", str(ex))
return False return
class NX584Alarm(alarm.AlarmControlPanel): class NX584Alarm(alarm.AlarmControlPanel):
@ -60,7 +60,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
# talk to the API and trigger a requests exception for setup_platform() # talk to the API and trigger a requests exception for setup_platform()
# to catch # to catch
self._alarm.list_zones() self._alarm.list_zones()
self._state = STATE_UNKNOWN self._state = None
@property @property
def name(self): def name(self):
@ -85,11 +85,11 @@ class NX584Alarm(alarm.AlarmControlPanel):
except requests.exceptions.ConnectionError as ex: except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to %(host)s: %(reason)s", _LOGGER.error("Unable to connect to %(host)s: %(reason)s",
dict(host=self._url, reason=ex)) dict(host=self._url, reason=ex))
self._state = STATE_UNKNOWN self._state = None
zones = [] zones = []
except IndexError: except IndexError:
_LOGGER.error("NX584 reports no partitions") _LOGGER.error("NX584 reports no partitions")
self._state = STATE_UNKNOWN self._state = None
zones = [] zones = []
bypassed = False bypassed = False
@ -107,6 +107,10 @@ class NX584Alarm(alarm.AlarmControlPanel):
else: else:
self._state = STATE_ALARM_ARMED_AWAY self._state = STATE_ALARM_ARMED_AWAY
for flag in part['condition_flags']:
if flag == "Siren on":
self._state = STATE_ALARM_TRIGGERED
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
self._alarm.disarm(code) self._alarm.disarm(code)

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['yalesmartalarmclient==0.1.5'] REQUIREMENTS = ['yalesmartalarmclient==0.1.6']
CONF_AREA_ID = 'area_id' CONF_AREA_ID = 'area_id'

View File

@ -32,6 +32,7 @@ CONF_DEVICE_TYPE = 'type'
CONF_PANEL_DISPLAY = 'panel_display' CONF_PANEL_DISPLAY = 'panel_display'
CONF_ZONE_NAME = 'name' CONF_ZONE_NAME = 'name'
CONF_ZONE_TYPE = 'type' CONF_ZONE_TYPE = 'type'
CONF_ZONE_LOOP = 'loop'
CONF_ZONE_RFID = 'rfid' CONF_ZONE_RFID = 'rfid'
CONF_ZONES = 'zones' CONF_ZONES = 'zones'
CONF_RELAY_ADDR = 'relayaddr' CONF_RELAY_ADDR = 'relayaddr'
@ -75,6 +76,8 @@ ZONE_SCHEMA = vol.Schema({
vol.Optional(CONF_ZONE_TYPE, vol.Optional(CONF_ZONE_TYPE,
default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA), default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA),
vol.Optional(CONF_ZONE_RFID): cv.string, vol.Optional(CONF_ZONE_RFID): cv.string,
vol.Optional(CONF_ZONE_LOOP):
vol.All(vol.Coerce(int), vol.Range(min=1, max=4)),
vol.Inclusive(CONF_RELAY_ADDR, 'relaylocation', vol.Inclusive(CONF_RELAY_ADDR, 'relaylocation',
'Relay address and channel must exist together'): cv.byte, 'Relay address and channel must exist together'): cv.byte,
vol.Inclusive(CONF_RELAY_CHAN, 'relaylocation', vol.Inclusive(CONF_RELAY_CHAN, 'relaylocation',

View File

@ -13,8 +13,9 @@ from homeassistant.helpers import entityfilter
from . import flash_briefings, intent, smart_home from . import flash_briefings, intent, smart_home
from .const import ( from .const import (
CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN, CONF_AUDIO, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY_URL,
CONF_FILTER, CONF_ENTITY_CONFIG) CONF_ENDPOINT, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN, CONF_FILTER,
CONF_ENTITY_CONFIG)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,6 +31,9 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({
}) })
SMART_HOME_SCHEMA = vol.Schema({ SMART_HOME_SCHEMA = vol.Schema({
vol.Optional(CONF_ENDPOINT): cv.string,
vol.Optional(CONF_CLIENT_ID): cv.string,
vol.Optional(CONF_CLIENT_SECRET): cv.string,
vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA, vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA} vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
}) })

View File

@ -0,0 +1,154 @@
"""Support for Alexa skill auth."""
import asyncio
import json
import logging
from datetime import timedelta
import aiohttp
import async_timeout
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from homeassistant.util import dt
from .const import DEFAULT_TIMEOUT
_LOGGER = logging.getLogger(__name__)
LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token"
LWA_HEADERS = {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
}
PREEMPTIVE_REFRESH_TTL_IN_SECONDS = 300
STORAGE_KEY = 'alexa_auth'
STORAGE_VERSION = 1
STORAGE_EXPIRE_TIME = "expire_time"
STORAGE_ACCESS_TOKEN = "access_token"
STORAGE_REFRESH_TOKEN = "refresh_token"
class Auth:
"""Handle authentication to send events to Alexa."""
def __init__(self, hass, client_id, client_secret):
"""Initialize the Auth class."""
self.hass = hass
self.client_id = client_id
self.client_secret = client_secret
self._prefs = None
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._get_token_lock = asyncio.Lock(loop=hass.loop)
async def async_do_auth(self, accept_grant_code):
"""Do authentication with an AcceptGrant code."""
# access token not retrieved yet for the first time, so this should
# be an access token request
lwa_params = {
"grant_type": "authorization_code",
"code": accept_grant_code,
"client_id": self.client_id,
"client_secret": self.client_secret
}
_LOGGER.debug("Calling LWA to get the access token (first time), "
"with: %s", json.dumps(lwa_params))
return await self._async_request_new_token(lwa_params)
async def async_get_access_token(self):
"""Perform access token or token refresh request."""
async with self._get_token_lock:
if self._prefs is None:
await self.async_load_preferences()
if self.is_token_valid():
_LOGGER.debug("Token still valid, using it.")
return self._prefs[STORAGE_ACCESS_TOKEN]
if self._prefs[STORAGE_REFRESH_TOKEN] is None:
_LOGGER.debug("Token invalid and no refresh token available.")
return None
lwa_params = {
"grant_type": "refresh_token",
"refresh_token": self._prefs[STORAGE_REFRESH_TOKEN],
"client_id": self.client_id,
"client_secret": self.client_secret
}
_LOGGER.debug("Calling LWA to refresh the access token.")
return await self._async_request_new_token(lwa_params)
@callback
def is_token_valid(self):
"""Check if a token is already loaded and if it is still valid."""
if not self._prefs[STORAGE_ACCESS_TOKEN]:
return False
expire_time = dt.parse_datetime(self._prefs[STORAGE_EXPIRE_TIME])
preemptive_expire_time = expire_time - timedelta(
seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS)
return dt.utcnow() < preemptive_expire_time
async def _async_request_new_token(self, lwa_params):
try:
session = aiohttp_client.async_get_clientsession(self.hass)
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self.hass.loop):
response = await session.post(LWA_TOKEN_URI,
headers=LWA_HEADERS,
data=lwa_params,
allow_redirects=True)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Timeout calling LWA to get auth token.")
return None
_LOGGER.debug("LWA response header: %s", response.headers)
_LOGGER.debug("LWA response status: %s", response.status)
if response.status != 200:
_LOGGER.error("Error calling LWA to get auth token.")
return None
response_json = await response.json()
_LOGGER.debug("LWA response body : %s", response_json)
access_token = response_json["access_token"]
refresh_token = response_json["refresh_token"]
expires_in = response_json["expires_in"]
expire_time = dt.utcnow() + timedelta(seconds=expires_in)
await self._async_update_preferences(access_token, refresh_token,
expire_time.isoformat())
return access_token
async def async_load_preferences(self):
"""Load preferences with stored tokens."""
self._prefs = await self._store.async_load()
if self._prefs is None:
self._prefs = {
STORAGE_ACCESS_TOKEN: None,
STORAGE_REFRESH_TOKEN: None,
STORAGE_EXPIRE_TIME: None
}
async def _async_update_preferences(self, access_token, refresh_token,
expire_time):
"""Update user preferences."""
if self._prefs is None:
await self.async_load_preferences()
if access_token is not None:
self._prefs[STORAGE_ACCESS_TOKEN] = access_token
if refresh_token is not None:
self._prefs[STORAGE_REFRESH_TOKEN] = refresh_token
if expire_time is not None:
self._prefs[STORAGE_EXPIRE_TIME] = expire_time
await self._store.async_save(self._prefs)

View File

@ -10,6 +10,9 @@ CONF_DISPLAY_URL = 'display_url'
CONF_FILTER = 'filter' CONF_FILTER = 'filter'
CONF_ENTITY_CONFIG = 'entity_config' CONF_ENTITY_CONFIG = 'entity_config'
CONF_ENDPOINT = 'endpoint'
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
ATTR_UID = 'uid' ATTR_UID = 'uid'
ATTR_UPDATE_DATE = 'updateDate' ATTR_UPDATE_DATE = 'updateDate'
@ -21,3 +24,5 @@ ATTR_REDIRECTION_URL = 'redirectionURL'
SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH' SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z' DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
DEFAULT_TIMEOUT = 30

View File

@ -5,15 +5,22 @@ https://developer.amazon.com/docs/smarthome/understand-the-smart-home-skill-api.
https://developer.amazon.com/docs/device-apis/message-guide.html https://developer.amazon.com/docs/device-apis/message-guide.html
""" """
import asyncio
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime from datetime import datetime
import json
import logging import logging
import math import math
from uuid import uuid4 from uuid import uuid4
import aiohttp
import async_timeout
from homeassistant.components import ( from homeassistant.components import (
alert, automation, binary_sensor, climate, cover, fan, group, http, alert, automation, binary_sensor, climate, cover, fan, group, http,
input_boolean, light, lock, media_player, scene, script, sensor, switch) input_boolean, light, lock, media_player, scene, script, sensor, switch)
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.event import async_track_state_change
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CLOUD_NEVER_EXPOSED_ENTITIES, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CLOUD_NEVER_EXPOSED_ENTITIES,
@ -21,13 +28,15 @@ from homeassistant.const import (
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON, STATE_UNLOCKED, SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON, STATE_UNLOCKED,
TEMP_CELSIUS, TEMP_FAHRENHEIT) TEMP_CELSIUS, TEMP_FAHRENHEIT, MATCH_ALL)
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.temperature import convert as convert_temperature
from .const import CONF_ENTITY_CONFIG, CONF_FILTER from .const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_ENDPOINT, \
CONF_ENTITY_CONFIG, CONF_FILTER, DATE_FORMAT, DEFAULT_TIMEOUT
from .auth import Auth
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,6 +46,8 @@ API_EVENT = 'event'
API_CONTEXT = 'context' API_CONTEXT = 'context'
API_HEADER = 'header' API_HEADER = 'header'
API_PAYLOAD = 'payload' API_PAYLOAD = 'payload'
API_SCOPE = 'scope'
API_CHANGE = 'change'
API_TEMP_UNITS = { API_TEMP_UNITS = {
TEMP_FAHRENHEIT: 'FAHRENHEIT', TEMP_FAHRENHEIT: 'FAHRENHEIT',
@ -66,6 +77,8 @@ HANDLERS = Registry()
ENTITY_ADAPTERS = Registry() ENTITY_ADAPTERS = Registry()
EVENT_ALEXA_SMART_HOME = 'alexa_smart_home' EVENT_ALEXA_SMART_HOME = 'alexa_smart_home'
AUTH_KEY = "alexa.smart_home.auth"
class _DisplayCategory: class _DisplayCategory:
"""Possible display categories for Discovery response. """Possible display categories for Discovery response.
@ -375,6 +388,8 @@ class _AlexaInterface:
'name': prop_name, 'name': prop_name,
'namespace': self.name(), 'namespace': self.name(),
'value': prop_value, 'value': prop_value,
'timeOfSample': datetime.now().strftime(DATE_FORMAT),
'uncertaintyInMilliseconds': 0
} }
@ -390,6 +405,9 @@ class _AlexaPowerController(_AlexaInterface):
def properties_supported(self): def properties_supported(self):
return [{'name': 'powerState'}] return [{'name': 'powerState'}]
def properties_proactively_reported(self):
return True
def properties_retrievable(self): def properties_retrievable(self):
return True return True
@ -417,6 +435,9 @@ class _AlexaLockController(_AlexaInterface):
def properties_retrievable(self): def properties_retrievable(self):
return True return True
def properties_proactively_reported(self):
return True
def get_property(self, name): def get_property(self, name):
if name != 'lockState': if name != 'lockState':
raise _UnsupportedProperty(name) raise _UnsupportedProperty(name)
@ -454,6 +475,9 @@ class _AlexaBrightnessController(_AlexaInterface):
def properties_supported(self): def properties_supported(self):
return [{'name': 'brightness'}] return [{'name': 'brightness'}]
def properties_proactively_reported(self):
return True
def properties_retrievable(self): def properties_retrievable(self):
return True return True
@ -585,6 +609,9 @@ class _AlexaTemperatureSensor(_AlexaInterface):
def properties_supported(self): def properties_supported(self):
return [{'name': 'temperature'}] return [{'name': 'temperature'}]
def properties_proactively_reported(self):
return True
def properties_retrievable(self): def properties_retrievable(self):
return True return True
@ -625,6 +652,9 @@ class _AlexaContactSensor(_AlexaInterface):
def properties_supported(self): def properties_supported(self):
return [{'name': 'detectionState'}] return [{'name': 'detectionState'}]
def properties_proactively_reported(self):
return True
def properties_retrievable(self): def properties_retrievable(self):
return True return True
@ -648,6 +678,9 @@ class _AlexaMotionSensor(_AlexaInterface):
def properties_supported(self): def properties_supported(self):
return [{'name': 'detectionState'}] return [{'name': 'detectionState'}]
def properties_proactively_reported(self):
return True
def properties_retrievable(self): def properties_retrievable(self):
return True return True
@ -686,6 +719,9 @@ class _AlexaThermostatController(_AlexaInterface):
properties.append({'name': 'thermostatMode'}) properties.append({'name': 'thermostatMode'})
return properties return properties
def properties_proactively_reported(self):
return True
def properties_retrievable(self): def properties_retrievable(self):
return True return True
@ -948,8 +984,11 @@ class _Cause:
class Config: class Config:
"""Hold the configuration for Alexa.""" """Hold the configuration for Alexa."""
def __init__(self, should_expose, entity_config=None): def __init__(self, endpoint, async_get_access_token, should_expose,
entity_config=None):
"""Initialize the configuration.""" """Initialize the configuration."""
self.endpoint = endpoint
self.async_get_access_token = async_get_access_token
self.should_expose = should_expose self.should_expose = should_expose
self.entity_config = entity_config or {} self.entity_config = entity_config or {}
@ -964,12 +1003,62 @@ def async_setup(hass, config):
Even if that's disabled, the functionality in this module may still be used Even if that's disabled, the functionality in this module may still be used
by the cloud component which will call async_handle_message directly. by the cloud component which will call async_handle_message directly.
""" """
if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET):
hass.data[AUTH_KEY] = Auth(hass, config[CONF_CLIENT_ID],
config[CONF_CLIENT_SECRET])
async_get_access_token = \
hass.data[AUTH_KEY].async_get_access_token if AUTH_KEY in hass.data \
else None
smart_home_config = Config( smart_home_config = Config(
endpoint=config.get(CONF_ENDPOINT),
async_get_access_token=async_get_access_token,
should_expose=config[CONF_FILTER], should_expose=config[CONF_FILTER],
entity_config=config.get(CONF_ENTITY_CONFIG), entity_config=config.get(CONF_ENTITY_CONFIG),
) )
hass.http.register_view(SmartHomeView(smart_home_config)) hass.http.register_view(SmartHomeView(smart_home_config))
if AUTH_KEY in hass.data:
hass.loop.create_task(
async_enable_proactive_mode(hass, smart_home_config))
async def async_enable_proactive_mode(hass, smart_home_config):
"""Enable the proactive mode.
Proactive mode makes this component report state changes to Alexa.
"""
if smart_home_config.async_get_access_token is None:
# no function to call to get token
return
if await smart_home_config.async_get_access_token() is None:
# not ready yet
return
async def async_entity_state_listener(changed_entity, old_state,
new_state):
if not smart_home_config.should_expose(changed_entity):
_LOGGER.debug("Not exposing %s because filtered by config",
changed_entity)
return
if new_state.domain not in ENTITY_ADAPTERS:
return
alexa_changed_entity = \
ENTITY_ADAPTERS[new_state.domain](hass, smart_home_config,
new_state)
for interface in alexa_changed_entity.interfaces():
if interface.properties_proactively_reported():
await async_send_changereport_message(hass, smart_home_config,
alexa_changed_entity)
return
async_track_state_change(hass, MATCH_ALL, async_entity_state_listener)
class SmartHomeView(http.HomeAssistantView): class SmartHomeView(http.HomeAssistantView):
"""Expose Smart Home v3 payload interface via HTTP POST.""" """Expose Smart Home v3 payload interface via HTTP POST."""
@ -1112,6 +1201,24 @@ class _AlexaResponse:
""" """
self._response[API_EVENT][API_HEADER]['correlationToken'] = token self._response[API_EVENT][API_HEADER]['correlationToken'] = token
def set_endpoint_full(self, bearer_token, endpoint_id, cookie=None):
"""Set the endpoint dictionary.
This is used to send proactive messages to Alexa.
"""
self._response[API_EVENT][API_ENDPOINT] = {
API_SCOPE: {
'type': 'BearerToken',
'token': bearer_token
}
}
if endpoint_id is not None:
self._response[API_EVENT][API_ENDPOINT]['endpointId'] = endpoint_id
if cookie is not None:
self._response[API_EVENT][API_ENDPOINT]['cookie'] = cookie
def set_endpoint(self, endpoint): def set_endpoint(self, endpoint):
"""Set the endpoint. """Set the endpoint.
@ -1222,6 +1329,62 @@ async def async_handle_message(
return response.serialize() return response.serialize()
async def async_send_changereport_message(hass, config, alexa_entity):
"""Send a ChangeReport message for an Alexa entity."""
token = await config.async_get_access_token()
if not token:
_LOGGER.error("Invalid access token.")
return
headers = {
"Authorization": "Bearer {}".format(token),
"Content-Type": "application/json;charset=UTF-8"
}
endpoint = alexa_entity.entity_id()
# this sends all the properties of the Alexa Entity, whether they have
# changed or not. this should be improved, and properties that have not
# changed should be moved to the 'context' object
properties = list(alexa_entity.serialize_properties())
payload = {
API_CHANGE: {
'cause': {'type': _Cause.APP_INTERACTION},
'properties': properties
}
}
message = _AlexaResponse(name='ChangeReport', namespace='Alexa',
payload=payload)
message.set_endpoint_full(token, endpoint)
message_str = json.dumps(message.serialize())
try:
session = aiohttp_client.async_get_clientsession(hass)
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
response = await session.post(config.endpoint,
headers=headers,
data=message_str,
allow_redirects=True)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Timeout calling LWA to get auth token.")
return None
response_text = await response.text()
_LOGGER.debug("Sent: %s", message_str)
_LOGGER.debug("Received (%s): %s", response.status, response_text)
if response.status != 202:
response_json = json.loads(response_text)
_LOGGER.error("Error when sending ChangeReport to Alexa: %s: %s",
response_json["payload"]["code"],
response_json["payload"]["description"])
@HANDLERS.register(('Alexa.Discovery', 'Discover')) @HANDLERS.register(('Alexa.Discovery', 'Discover'))
async def async_api_discovery(hass, config, directive, context): async def async_api_discovery(hass, config, directive, context):
"""Create a API formatted discovery response. """Create a API formatted discovery response.
@ -1258,8 +1421,9 @@ async def async_api_discovery(hass, config, directive, context):
i.serialize_discovery() for i in alexa_entity.interfaces()] i.serialize_discovery() for i in alexa_entity.interfaces()]
if not endpoint['capabilities']: if not endpoint['capabilities']:
_LOGGER.debug("Not exposing %s because it has no capabilities", _LOGGER.debug(
entity.entity_id) "Not exposing %s because it has no capabilities",
entity.entity_id)
continue continue
discovery_endpoints.append(endpoint) discovery_endpoints.append(endpoint)
@ -1270,6 +1434,25 @@ async def async_api_discovery(hass, config, directive, context):
) )
@HANDLERS.register(('Alexa.Authorization', 'AcceptGrant'))
async def async_api_accept_grant(hass, config, directive, context):
"""Create a API formatted AcceptGrant response.
Async friendly.
"""
auth_code = directive.payload['grant']['code']
_LOGGER.debug("AcceptGrant code: %s", auth_code)
if AUTH_KEY in hass.data:
await hass.data[AUTH_KEY].async_do_auth(auth_code)
await async_enable_proactive_mode(hass, config)
return directive.response(
name='AcceptGrant.Response',
namespace='Alexa.Authorization',
payload={})
@HANDLERS.register(('Alexa.PowerController', 'TurnOn')) @HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
async def async_api_turn_on(hass, config, directive, context): async def async_api_turn_on(hass, config, directive, context):
"""Process a turn on request.""" """Process a turn on request."""

View File

@ -16,7 +16,7 @@ from homeassistant.const import (
from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
REQUIREMENTS = ['pyarlo==0.2.2'] REQUIREMENTS = ['pyarlo==0.2.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -14,7 +14,7 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.discovery import async_load_platform
REQUIREMENTS = ['aioasuswrt==1.1.15'] REQUIREMENTS = ['aioasuswrt==1.1.17']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -5,28 +5,28 @@
"no_available_service": "No hi ha serveis de notificaci\u00f3 disponibles." "no_available_service": "No hi ha serveis de notificaci\u00f3 disponibles."
}, },
"error": { "error": {
"invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho." "invalid_code": "Codi inv\u00e0lid, si us plau torna a provar-ho."
}, },
"step": { "step": {
"init": { "init": {
"description": "Seleccioneu un dels serveis de notificaci\u00f3:", "description": "Selecciona un dels serveis de notificaci\u00f3:",
"title": "Configureu una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions" "title": "Configuraci\u00f3 d'una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions"
}, },
"setup": { "setup": {
"description": "S'ha enviat una contrasenya d'un sol \u00fas mitjan\u00e7ant **notify.{notify_service}**. Introdu\u00efu-la a continuaci\u00f3:", "description": "S'ha enviat una contrasenya d'un sol \u00fas mitjan\u00e7ant **notify.{notify_service}**. Introdueix-la a continuaci\u00f3:",
"title": "Verifiqueu la configuraci\u00f3" "title": "Verificaci\u00f3 de la configuraci\u00f3"
} }
}, },
"title": "Contrasenya d'un sol \u00fas del servei de notificacions" "title": "Contrasenya d'un sol \u00fas del servei de notificacions"
}, },
"totp": { "totp": {
"error": { "error": {
"invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa." "invalid_code": "Codi inv\u00e0lid, si us plau torna a provar-ho. Si obtens aquest error repetidament, assegura't que la data i hora de Home Assistant siguin correctes i acurades."
}, },
"step": { "step": {
"init": { "init": {
"description": "Per activar la verificaci\u00f3 en dos passos mitjan\u00e7ant contrasenyes d'un sol \u00fas basades en temps, escanegeu el codi QR amb la vostre aplicaci\u00f3 de verificaci\u00f3. Si no en teniu cap, us recomanem [Google Authenticator](https://support.google.com/accounts/answer/1066447) o b\u00e9 [Authy](https://authy.com/). \n\n {qr_code} \n \nDespr\u00e9s d'escanejar el codi QR, introdu\u00efu el codi de sis d\u00edgits proporcionat per l'aplicaci\u00f3. Si teniu problemes per escanejar el codi QR, feu una configuraci\u00f3 manual amb el codi **`{code}`**.", "description": "Per activar la verificaci\u00f3 en dos passos mitjan\u00e7ant contrasenyes d'un sol \u00fas basades en temps, escaneja el codi QR amb la teva aplicaci\u00f3 de verificaci\u00f3. Si no en tens cap, et recomanem [Google Authenticator](https://support.google.com/accounts/answer/1066447) o b\u00e9 [Authy](https://authy.com/). \n\n {qr_code} \n \nDespr\u00e9s d'escanejar el codi QR, introdueix el codi de sis d\u00edgits proporcionat per l'aplicaci\u00f3. Si tens problemes per escanejar el codi QR, fes una configuraci\u00f3 manual amb el codi **`{code}`**.",
"title": "Configureu la verificaci\u00f3 en dos passos utilitzant TOTP" "title": "Configura la verificaci\u00f3 en dos passos utilitzant TOTP"
} }
}, },
"title": "TOTP" "title": "TOTP"

View File

@ -94,11 +94,11 @@ PLATFORM_SCHEMA = vol.Schema({
}) })
SERVICE_SCHEMA = vol.Schema({ SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
}) })
TRIGGER_SERVICE_SCHEMA = vol.Schema({ TRIGGER_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids,
vol.Optional(ATTR_VARIABLES, default={}): dict, vol.Optional(ATTR_VARIABLES, default={}): dict,
}) })
@ -375,7 +375,15 @@ def _async_get_action(hass, config, name):
async def action(entity_id, variables, context): async def action(entity_id, variables, context):
"""Execute an action.""" """Execute an action."""
_LOGGER.info('Executing %s', name) _LOGGER.info('Executing %s', name)
await script_obj.async_run(variables, context) hass.components.logbook.async_log_entry(
name, 'has been triggered', DOMAIN, entity_id)
try:
await script_obj.async_run(variables, context)
except Exception as err: # pylint: disable=broad-except
script_obj.async_log_exception(
_LOGGER,
'Error while executing automation {}'.format(entity_id), err)
return action return action

View File

@ -9,7 +9,7 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.alarmdecoder import ( from homeassistant.components.alarmdecoder import (
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE, ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE, CONF_ZONE_RFID, CONF_ZONE_LOOP, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
SIGNAL_RFX_MESSAGE, SIGNAL_REL_MESSAGE, CONF_RELAY_ADDR, SIGNAL_RFX_MESSAGE, SIGNAL_REL_MESSAGE, CONF_RELAY_ADDR,
CONF_RELAY_CHAN) CONF_RELAY_CHAN)
@ -37,10 +37,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
zone_type = device_config_data[CONF_ZONE_TYPE] zone_type = device_config_data[CONF_ZONE_TYPE]
zone_name = device_config_data[CONF_ZONE_NAME] zone_name = device_config_data[CONF_ZONE_NAME]
zone_rfid = device_config_data.get(CONF_ZONE_RFID) zone_rfid = device_config_data.get(CONF_ZONE_RFID)
zone_loop = device_config_data.get(CONF_ZONE_LOOP)
relay_addr = device_config_data.get(CONF_RELAY_ADDR) relay_addr = device_config_data.get(CONF_RELAY_ADDR)
relay_chan = device_config_data.get(CONF_RELAY_CHAN) relay_chan = device_config_data.get(CONF_RELAY_CHAN)
device = AlarmDecoderBinarySensor( device = AlarmDecoderBinarySensor(
zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan) zone_num, zone_name, zone_type, zone_rfid, zone_loop, relay_addr,
relay_chan)
devices.append(device) devices.append(device)
add_entities(devices) add_entities(devices)
@ -51,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class AlarmDecoderBinarySensor(BinarySensorDevice): class AlarmDecoderBinarySensor(BinarySensorDevice):
"""Representation of an AlarmDecoder binary sensor.""" """Representation of an AlarmDecoder binary sensor."""
def __init__(self, zone_number, zone_name, zone_type, zone_rfid, def __init__(self, zone_number, zone_name, zone_type, zone_rfid, zone_loop,
relay_addr, relay_chan): relay_addr, relay_chan):
"""Initialize the binary_sensor.""" """Initialize the binary_sensor."""
self._zone_number = zone_number self._zone_number = zone_number
@ -59,6 +61,7 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
self._state = None self._state = None
self._name = zone_name self._name = zone_name
self._rfid = zone_rfid self._rfid = zone_rfid
self._loop = zone_loop
self._rfstate = None self._rfstate = None
self._relay_addr = relay_addr self._relay_addr = relay_addr
self._relay_chan = relay_chan self._relay_chan = relay_chan
@ -92,14 +95,14 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
"""Return the state attributes.""" """Return the state attributes."""
attr = {} attr = {}
if self._rfid and self._rfstate is not None: if self._rfid and self._rfstate is not None:
attr[ATTR_RF_BIT0] = True if self._rfstate & 0x01 else False attr[ATTR_RF_BIT0] = bool(self._rfstate & 0x01)
attr[ATTR_RF_LOW_BAT] = True if self._rfstate & 0x02 else False attr[ATTR_RF_LOW_BAT] = bool(self._rfstate & 0x02)
attr[ATTR_RF_SUPERVISED] = True if self._rfstate & 0x04 else False attr[ATTR_RF_SUPERVISED] = bool(self._rfstate & 0x04)
attr[ATTR_RF_BIT3] = True if self._rfstate & 0x08 else False attr[ATTR_RF_BIT3] = bool(self._rfstate & 0x08)
attr[ATTR_RF_LOOP3] = True if self._rfstate & 0x10 else False attr[ATTR_RF_LOOP3] = bool(self._rfstate & 0x10)
attr[ATTR_RF_LOOP2] = True if self._rfstate & 0x20 else False attr[ATTR_RF_LOOP2] = bool(self._rfstate & 0x20)
attr[ATTR_RF_LOOP4] = True if self._rfstate & 0x40 else False attr[ATTR_RF_LOOP4] = bool(self._rfstate & 0x40)
attr[ATTR_RF_LOOP1] = True if self._rfstate & 0x80 else False attr[ATTR_RF_LOOP1] = bool(self._rfstate & 0x80)
return attr return attr
@property @property
@ -128,6 +131,8 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
"""Update RF state.""" """Update RF state."""
if self._rfid and message and message.serial_number == self._rfid: if self._rfid and message and message.serial_number == self._rfid:
self._rfstate = message.value self._rfstate = message.value
if self._loop:
self._state = 1 if message.loop[self._loop - 1] else 0
self.schedule_update_ha_state() self.schedule_update_ha_state()
def _rel_message_callback(self, message): def _rel_message_callback(self, message):

View File

@ -0,0 +1,63 @@
"""Support for ESPHome binary sensors."""
import logging
from typing import TYPE_CHECKING, Optional
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.esphome import EsphomeEntity, \
platform_async_setup_entry
if TYPE_CHECKING:
# pylint: disable=unused-import
from aioesphomeapi import BinarySensorInfo, BinarySensorState # noqa
DEPENDENCIES = ['esphome']
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up ESPHome binary sensors based on a config entry."""
# pylint: disable=redefined-outer-name
from aioesphomeapi import BinarySensorInfo, BinarySensorState # noqa
await platform_async_setup_entry(
hass, entry, async_add_entities,
component_key='binary_sensor',
info_type=BinarySensorInfo, entity_type=EsphomeBinarySensor,
state_type=BinarySensorState
)
class EsphomeBinarySensor(EsphomeEntity, BinarySensorDevice):
"""A binary sensor implementation for ESPHome."""
@property
def _static_info(self) -> 'BinarySensorInfo':
return super()._static_info
@property
def _state(self) -> Optional['BinarySensorState']:
return super()._state
@property
def is_on(self):
"""Return true if the binary sensor is on."""
if self._static_info.is_status_binary_sensor:
# Status binary sensors indicated connected state.
# So in their case what's usually _availability_ is now state
return self._entry_data.available
if self._state is None:
return None
return self._state.state
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return self._static_info.device_class
@property
def available(self):
"""Return True if entity is available."""
if self._static_info.is_status_binary_sensor:
return True
return super().available

View File

@ -10,6 +10,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDevice, ENTITY_ID_FORMAT) BinarySensorDevice, ENTITY_ID_FORMAT)
from homeassistant.components.fibaro import ( from homeassistant.components.fibaro import (
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice)
from homeassistant.const import (CONF_DEVICE_CLASS, CONF_ICON)
DEPENDENCIES = ['fibaro'] DEPENDENCIES = ['fibaro']
@ -45,6 +46,7 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorDevice):
super().__init__(fibaro_device, controller) super().__init__(fibaro_device, controller)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
stype = None stype = None
devconf = fibaro_device.device_config
if fibaro_device.type in SENSOR_TYPES: if fibaro_device.type in SENSOR_TYPES:
stype = fibaro_device.type stype = fibaro_device.type
elif fibaro_device.baseType in SENSOR_TYPES: elif fibaro_device.baseType in SENSOR_TYPES:
@ -55,6 +57,10 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorDevice):
else: else:
self._device_class = None self._device_class = None
self._icon = None self._icon = None
# device_config overrides:
self._device_class = devconf.get(CONF_DEVICE_CLASS,
self._device_class)
self._icon = devconf.get(CONF_ICON, self._icon)
@property @property
def icon(self): def icon(self):

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.8'] REQUIREMENTS = ['pyhik==0.1.9']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_IGNORED = 'ignored' CONF_IGNORED = 'ignored'

View File

@ -28,14 +28,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the HomematicIP Cloud binary sensor from a config entry.""" """Set up the HomematicIP Cloud binary sensor from a config entry."""
from homematicip.aio.device import ( from homematicip.aio.device import (
AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector, AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector,
AsyncWaterSensor, AsyncRotaryHandleSensor) AsyncWaterSensor, AsyncRotaryHandleSensor,
AsyncMotionDetectorPushButton)
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = [] devices = []
for device in home.devices: for device in home.devices:
if isinstance(device, (AsyncShutterContact, AsyncRotaryHandleSensor)): if isinstance(device, (AsyncShutterContact, AsyncRotaryHandleSensor)):
devices.append(HomematicipShutterContact(home, device)) devices.append(HomematicipShutterContact(home, device))
elif isinstance(device, AsyncMotionDetectorIndoor): elif isinstance(device, (AsyncMotionDetectorIndoor,
AsyncMotionDetectorPushButton)):
devices.append(HomematicipMotionDetector(home, device)) devices.append(HomematicipMotionDetector(home, device))
elif isinstance(device, AsyncSmokeDetector): elif isinstance(device, AsyncSmokeDetector):
devices.append(HomematicipSmokeDetector(home, device)) devices.append(HomematicipSmokeDetector(home, device))

View File

@ -14,6 +14,7 @@ DEPENDENCIES = ['insteon']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {'openClosedSensor': 'opening', SENSOR_TYPES = {'openClosedSensor': 'opening',
'ioLincSensor': 'opening',
'motionSensor': 'motion', 'motionSensor': 'motion',
'doorSensor': 'door', 'doorSensor': 'door',
'wetLeakSensor': 'moisture', 'wetLeakSensor': 'moisture',
@ -58,7 +59,7 @@ class InsteonBinarySensor(InsteonEntity, BinarySensorDevice):
on_val = bool(self._insteon_device_state.value) on_val = bool(self._insteon_device_state.value)
if self._insteon_device_state.name in ['lightSensor', if self._insteon_device_state.name in ['lightSensor',
'openClosedSensor']: 'ioLincSensor']:
return not on_val return not on_val
return on_val return on_val

View File

@ -52,7 +52,7 @@ def setup_platform(hass, config: ConfigType,
node.nid, node.parent_nid) node.nid, node.parent_nid)
else: else:
device_type = _detect_device_type(node) device_type = _detect_device_type(node)
subnode_id = int(node.nid[-1]) subnode_id = int(node.nid[-1], 16)
if device_type in ('opening', 'moisture'): if device_type in ('opening', 'moisture'):
# These sensors use an optional "negative" subnode 2 to snag # These sensors use an optional "negative" subnode 2 to snag
# all state changes # all state changes

View File

@ -16,10 +16,10 @@ from homeassistant.const import (
CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON,
CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS, CONF_DEVICE) CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS, CONF_DEVICE)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate,
subscription) MqttEntityDeviceInfo, subscription)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -49,7 +49,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
# This is an exception because MQTT is a message transport, not a protocol # This is an exception because MQTT is a message transport, not a protocol
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend(
mqtt.MQTT_JSON_ATTRS_SCHEMA.schema)
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
@ -76,7 +77,7 @@ async def _async_setup_entity(config, async_add_entities, discovery_hash=None):
async_add_entities([MqttBinarySensor(config, discovery_hash)]) async_add_entities([MqttBinarySensor(config, discovery_hash)])
class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, class MqttBinarySensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate,
MqttEntityDeviceInfo, BinarySensorDevice): MqttEntityDeviceInfo, BinarySensorDevice):
"""Representation a binary sensor that is updated by MQTT.""" """Representation a binary sensor that is updated by MQTT."""
@ -94,6 +95,7 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
qos = config.get(CONF_QOS) qos = config.get(CONF_QOS)
device_config = config.get(CONF_DEVICE) device_config = config.get(CONF_DEVICE)
MqttAttributes.__init__(self, config)
MqttAvailability.__init__(self, availability_topic, qos, MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available) payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash, MqttDiscoveryUpdate.__init__(self, discovery_hash,
@ -109,6 +111,7 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
"""Handle updated discovery message.""" """Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload) config = PLATFORM_SCHEMA(discovery_payload)
self._config = config self._config = config
await self.attributes_discovery_update(config)
await self.availability_discovery_update(config) await self.availability_discovery_update(config)
await self._subscribe_topics() await self._subscribe_topics()
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@ -132,7 +135,7 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
value_template = self._config.get(CONF_VALUE_TEMPLATE) value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None: if value_template is not None:
payload = value_template.async_render_with_possible_json_value( payload = value_template.async_render_with_possible_json_value(
payload) payload, variables={'entity_id': self.entity_id})
if payload == self._config.get(CONF_PAYLOAD_ON): if payload == self._config.get(CONF_PAYLOAD_ON):
self._state = True self._state = True
elif payload == self._config.get(CONF_PAYLOAD_OFF): elif payload == self._config.get(CONF_PAYLOAD_OFF):
@ -163,7 +166,9 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self):
"""Unsubscribe when removed.""" """Unsubscribe when removed."""
await subscription.async_unsubscribe_topics(self.hass, self._sub_state) self._sub_state = await subscription.async_unsubscribe_topics(
self.hass, self._sub_state)
await MqttAttributes.async_will_remove_from_hass(self)
await MqttAvailability.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self)
@property @property

View File

@ -61,8 +61,7 @@ class MyStromView(HomeAssistantView):
'{}_{}'.format(button_id, button_action)) '{}_{}'.format(button_id, button_action))
self.add_entities([self.buttons[entity_id]]) self.add_entities([self.buttons[entity_id]])
else: else:
new_state = True if self.buttons[entity_id].state == 'off' \ new_state = self.buttons[entity_id].state == 'off'
else False
self.buttons[entity_id].async_on_update(new_state) self.buttons[entity_id].async_on_update(new_state)

View File

@ -0,0 +1,81 @@
"""
Support for Ness D8X/D16X zone states - represented as binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.ness_alarm/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.ness_alarm import (
CONF_ZONES, CONF_ZONE_TYPE, CONF_ZONE_NAME, CONF_ZONE_ID,
SIGNAL_ZONE_CHANGED, ZoneChangedData)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['ness_alarm']
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Ness Alarm binary sensor devices."""
if not discovery_info:
return
configured_zones = discovery_info[CONF_ZONES]
devices = []
for zone_config in configured_zones:
zone_type = zone_config[CONF_ZONE_TYPE]
zone_name = zone_config[CONF_ZONE_NAME]
zone_id = zone_config[CONF_ZONE_ID]
device = NessZoneBinarySensor(zone_id=zone_id, name=zone_name,
zone_type=zone_type)
devices.append(device)
async_add_entities(devices)
class NessZoneBinarySensor(BinarySensorDevice):
"""Representation of an Ness alarm zone as a binary sensor."""
def __init__(self, zone_id, name, zone_type):
"""Initialize the binary_sensor."""
self._zone_id = zone_id
self._name = name
self._type = zone_type
self._state = 0
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_ZONE_CHANGED, self._handle_zone_change)
@property
def name(self):
"""Return the name of the entity."""
return self._name
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def is_on(self):
"""Return true if sensor is on."""
return self._state == 1
@property
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
return self._type
@callback
def _handle_zone_change(self, data: ZoneChangedData):
"""Handle zone state update."""
if self._zone_id == data.zone_id:
self._state = data.state
self.async_schedule_update_ha_state()

View File

@ -7,8 +7,7 @@ https://home-assistant.io/components/binary_sensor.point/
import logging import logging
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
DOMAIN as PARENT_DOMAIN, BinarySensorDevice)
from homeassistant.components.point import MinutPointEntity from homeassistant.components.point import MinutPointEntity
from homeassistant.components.point.const import ( from homeassistant.components.point.const import (
DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK) DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK)
@ -49,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for device_class in EVENTS), True) for device_class in EVENTS), True)
async_dispatcher_connect( async_dispatcher_connect(
hass, POINT_DISCOVERY_NEW.format(PARENT_DOMAIN, POINT_DOMAIN), hass, POINT_DISCOVERY_NEW.format(DOMAIN, POINT_DOMAIN),
async_discover_sensor) async_discover_sensor)

View File

@ -8,9 +8,11 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.satel_integra import (CONF_ZONES, from homeassistant.components.satel_integra import (CONF_ZONES,
CONF_OUTPUTS,
CONF_ZONE_NAME, CONF_ZONE_NAME,
CONF_ZONE_TYPE, CONF_ZONE_TYPE,
SIGNAL_ZONES_UPDATED) SIGNAL_ZONES_UPDATED,
SIGNAL_OUTPUTS_UPDATED)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -32,7 +34,17 @@ async def async_setup_platform(hass, config, async_add_entities,
for zone_num, device_config_data in configured_zones.items(): for zone_num, device_config_data in configured_zones.items():
zone_type = device_config_data[CONF_ZONE_TYPE] zone_type = device_config_data[CONF_ZONE_TYPE]
zone_name = device_config_data[CONF_ZONE_NAME] zone_name = device_config_data[CONF_ZONE_NAME]
device = SatelIntegraBinarySensor(zone_num, zone_name, zone_type) device = SatelIntegraBinarySensor(zone_num, zone_name, zone_type,
SIGNAL_ZONES_UPDATED)
devices.append(device)
configured_outputs = discovery_info[CONF_OUTPUTS]
for zone_num, device_config_data in configured_outputs.items():
zone_type = device_config_data[CONF_ZONE_TYPE]
zone_name = device_config_data[CONF_ZONE_NAME]
device = SatelIntegraBinarySensor(zone_num, zone_name, zone_type,
SIGNAL_OUTPUTS_UPDATED)
devices.append(device) devices.append(device)
async_add_entities(devices) async_add_entities(devices)
@ -41,17 +53,18 @@ async def async_setup_platform(hass, config, async_add_entities,
class SatelIntegraBinarySensor(BinarySensorDevice): class SatelIntegraBinarySensor(BinarySensorDevice):
"""Representation of an Satel Integra binary sensor.""" """Representation of an Satel Integra binary sensor."""
def __init__(self, zone_number, zone_name, zone_type): def __init__(self, device_number, device_name, zone_type, react_to_signal):
"""Initialize the binary_sensor.""" """Initialize the binary_sensor."""
self._zone_number = zone_number self._device_number = device_number
self._name = zone_name self._name = device_name
self._zone_type = zone_type self._zone_type = zone_type
self._state = 0 self._state = 0
self._react_to_signal = react_to_signal
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
async_dispatcher_connect( async_dispatcher_connect(
self.hass, SIGNAL_ZONES_UPDATED, self._zones_updated) self.hass, self._react_to_signal, self._devices_updated)
@property @property
def name(self): def name(self):
@ -80,9 +93,9 @@ class SatelIntegraBinarySensor(BinarySensorDevice):
return self._zone_type return self._zone_type
@callback @callback
def _zones_updated(self, zones): def _devices_updated(self, zones):
"""Update the zone's state, if needed.""" """Update the zone's state, if needed."""
if self._zone_number in zones \ if self._device_number in zones \
and self._state != zones[self._zone_number]: and self._state != zones[self._device_number]:
self._state = zones[self._zone_number] self._state = zones[self._device_number]
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()

View File

@ -9,22 +9,35 @@ https://home-assistant.io/components/binary_sensor.tellduslive/
""" """
import logging import logging
from homeassistant.components import tellduslive from homeassistant.components import binary_sensor, tellduslive
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.tellduslive.entry import TelldusLiveEntity from homeassistant.components.tellduslive.entry import TelldusLiveEntity
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up Tellstick sensors.""" """Old way of setting up TelldusLive.
if discovery_info is None:
return Can only be called when a user accidentally mentions the platform in their
client = hass.data[tellduslive.DOMAIN] config. But even in that case it would have been ignored.
add_entities( """
TelldusLiveSensor(client, binary_sensor) pass
for binary_sensor in discovery_info
)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up tellduslive sensors dynamically."""
async def async_discover_binary_sensor(device_id):
"""Discover and add a discovered sensor."""
client = hass.data[tellduslive.DOMAIN]
async_add_entities([TelldusLiveSensor(client, device_id)])
async_dispatcher_connect(
hass,
tellduslive.TELLDUS_DISCOVERY_NEW.format(binary_sensor.DOMAIN,
tellduslive.DOMAIN),
async_discover_binary_sensor)
class TelldusLiveSensor(TelldusLiveEntity, BinarySensorDevice): class TelldusLiveSensor(TelldusLiveEntity, BinarySensorDevice):

View File

@ -4,7 +4,10 @@ Support for WeMo sensors.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.wemo/ https://home-assistant.io/components/binary_sensor.wemo/
""" """
import asyncio
import logging import logging
import async_timeout
import requests import requests
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
@ -15,7 +18,7 @@ DEPENDENCIES = ['wemo']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities_callback, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Register discovered WeMo binary sensors.""" """Register discovered WeMo binary sensors."""
from pywemo import discovery from pywemo import discovery
@ -31,7 +34,7 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None):
raise PlatformNotReady raise PlatformNotReady
if device: if device:
add_entities_callback([WemoBinarySensor(hass, device)]) add_entities([WemoBinarySensor(hass, device)])
class WemoBinarySensor(BinarySensorDevice): class WemoBinarySensor(BinarySensorDevice):
@ -41,48 +44,90 @@ class WemoBinarySensor(BinarySensorDevice):
"""Initialize the WeMo sensor.""" """Initialize the WeMo sensor."""
self.wemo = device self.wemo = device
self._state = None self._state = None
self._available = True
self._update_lock = None
self._model_name = self.wemo.model_name
self._name = self.wemo.name
self._serialnumber = self.wemo.serialnumber
wemo = hass.components.wemo def _subscription_callback(self, _device, _type, _params):
wemo.SUBSCRIPTION_REGISTRY.register(self.wemo) """Update the state by the Wemo sensor."""
wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback) _LOGGER.debug("Subscription update for %s", self.name)
def _update_callback(self, _device, _type, _params):
"""Handle state changes."""
_LOGGER.info("Subscription update for %s", _device)
updated = self.wemo.subscription_update(_type, _params) updated = self.wemo.subscription_update(_type, _params)
self._update(force_update=(not updated)) self.hass.add_job(
self._async_locked_subscription_callback(not updated))
if not hasattr(self, 'hass'): async def _async_locked_subscription_callback(self, force_update):
"""Handle an update from a subscription."""
# If an update is in progress, we don't do anything
if self._update_lock.locked():
return return
self.schedule_update_ha_state()
@property await self._async_locked_update(force_update)
def should_poll(self): self.async_schedule_update_ha_state()
"""No polling needed with subscriptions."""
return False async def async_added_to_hass(self):
"""Wemo sensor added to HASS."""
# Define inside async context so we know our event loop
self._update_lock = asyncio.Lock()
registry = self.hass.components.wemo.SUBSCRIPTION_REGISTRY
await self.hass.async_add_executor_job(registry.register, self.wemo)
registry.on(self.wemo, None, self._subscription_callback)
async def async_update(self):
"""Update WeMo state.
Wemo has an aggressive retry logic that sometimes can take over a
minute to return. If we don't get a state after 5 seconds, assume the
Wemo sensor is unreachable. If update goes through, it will be made
available again.
"""
# If an update is in progress, we don't do anything
if self._update_lock.locked():
return
try:
with async_timeout.timeout(5):
await asyncio.shield(self._async_locked_update(True))
except asyncio.TimeoutError:
_LOGGER.warning('Lost connection to %s', self.name)
self._available = False
async def _async_locked_update(self, force_update):
"""Try updating within an async lock."""
async with self._update_lock:
await self.hass.async_add_executor_job(self._update, force_update)
def _update(self, force_update=True):
"""Update the sensor state."""
try:
self._state = self.wemo.get_state(force_update)
if not self._available:
_LOGGER.info('Reconnected to %s', self.name)
self._available = True
except AttributeError as err:
_LOGGER.warning("Could not update status for %s (%s)",
self.name, err)
self._available = False
@property @property
def unique_id(self): def unique_id(self):
"""Return the id of this WeMo device.""" """Return the id of this WeMo sensor."""
return self.wemo.serialnumber return self._serialnumber
@property @property
def name(self): def name(self):
"""Return the name of the service if any.""" """Return the name of the service if any."""
return self.wemo.name return self._name
@property @property
def is_on(self): def is_on(self):
"""Return true if sensor is on.""" """Return true if sensor is on."""
return self._state return self._state
def update(self): @property
"""Update WeMo state.""" def available(self):
self._update(force_update=True) """Return true if sensor is available."""
return self._available
def _update(self, force_update=True):
try:
self._state = self.wemo.get_state(force_update)
except AttributeError as err:
_LOGGER.warning(
"Could not update status for %s (%s)", self.name, err)

View File

@ -409,10 +409,14 @@ class XiaomiButton(XiaomiBinarySensor):
click_type = 'double' click_type = 'double'
elif value == 'both_click': elif value == 'both_click':
click_type = 'both' click_type = 'both'
elif value == 'double_both_click':
click_type = 'double_both'
elif value == 'shake': elif value == 'shake':
click_type = 'shake' click_type = 'shake'
elif value in ['long_click', 'long_both_click']: elif value == 'long_click':
return False click_type = 'long'
elif value == 'long_both_click':
click_type = 'long_both'
else: else:
_LOGGER.warning("Unsupported click_type detected: %s", value) _LOGGER.warning("Unsupported click_type detected: %s", value)
return False return False
@ -465,4 +469,12 @@ class XiaomiCube(XiaomiBinarySensor):
}) })
self._last_action = 'rotate' self._last_action = 'rotate'
if 'rotate_degree' in data:
self._hass.bus.fire('xiaomi_aqara.cube_action', {
'entity_id': self.entity_id,
'action_type': 'rotate',
'action_value': float(data['rotate_degree'].replace(",", "."))
})
self._last_action = 'rotate'
return True return True

View File

@ -9,9 +9,14 @@ import logging
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
from homeassistant.components.zha import helpers from homeassistant.components.zha import helpers
from homeassistant.components.zha.const import ( from homeassistant.components.zha.const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW) DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW)
from homeassistant.components.zha.entities import ZhaEntity from homeassistant.components.zha.entities import ZhaEntity
from homeassistant.const import STATE_ON
from homeassistant.components.zha.entities.listeners import (
OnOffListener, LevelListener
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.restore_state import RestoreEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -26,6 +31,7 @@ CLASS_MAPPING = {
0x002b: 'gas', 0x002b: 'gas',
0x002d: 'vibration', 0x002d: 'vibration',
} }
DEVICE_CLASS_OCCUPANCY = 'occupancy'
async def async_setup_platform(hass, config, async_add_entities, async def async_setup_platform(hass, config, async_add_entities,
@ -54,14 +60,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entities(hass, config_entry, async_add_entities, async def _async_setup_entities(hass, config_entry, async_add_entities,
discovery_infos): discovery_infos):
"""Set up the ZHA binary sensors.""" """Set up the ZHA binary sensors."""
from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.measurement import OccupancySensing
from zigpy.zcl.clusters.security import IasZone
entities = [] entities = []
for discovery_info in discovery_infos: for discovery_info in discovery_infos:
from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.security import IasZone
if IasZone.cluster_id in discovery_info['in_clusters']: if IasZone.cluster_id in discovery_info['in_clusters']:
entities.append(await _async_setup_iaszone(discovery_info)) entities.append(await _async_setup_iaszone(discovery_info))
elif OccupancySensing.cluster_id in discovery_info['in_clusters']:
entities.append(
BinarySensor(DEVICE_CLASS_OCCUPANCY, **discovery_info))
elif OnOff.cluster_id in discovery_info['out_clusters']: elif OnOff.cluster_id in discovery_info['out_clusters']:
entities.append(await _async_setup_remote(discovery_info)) entities.append(Remote(**discovery_info))
async_add_entities(entities, update_before_add=True) async_add_entities(entities, update_before_add=True)
@ -70,10 +81,6 @@ async def _async_setup_iaszone(discovery_info):
device_class = None device_class = None
from zigpy.zcl.clusters.security import IasZone from zigpy.zcl.clusters.security import IasZone
cluster = discovery_info['in_clusters'][IasZone.cluster_id] cluster = discovery_info['in_clusters'][IasZone.cluster_id]
if discovery_info['new_join']:
await cluster.bind()
ieee = cluster.endpoint.device.application.ieee
await cluster.write_attributes({'cie_addr': ieee})
try: try:
zone_type = await cluster['zone_type'] zone_type = await cluster['zone_type']
@ -82,33 +89,11 @@ async def _async_setup_iaszone(discovery_info):
# If we fail to read from the device, use a non-specific class # If we fail to read from the device, use a non-specific class
pass pass
return BinarySensor(device_class, **discovery_info) return IasZoneSensor(device_class, **discovery_info)
async def _async_setup_remote(discovery_info): class IasZoneSensor(RestoreEntity, ZhaEntity, BinarySensorDevice):
remote = Remote(**discovery_info) """The IasZoneSensor Binary Sensor."""
if discovery_info['new_join']:
from zigpy.zcl.clusters.general import OnOff, LevelControl
out_clusters = discovery_info['out_clusters']
if OnOff.cluster_id in out_clusters:
cluster = out_clusters[OnOff.cluster_id]
await helpers.configure_reporting(
remote.entity_id, cluster, 0, min_report=0, max_report=600,
reportable_change=1
)
if LevelControl.cluster_id in out_clusters:
cluster = out_clusters[LevelControl.cluster_id]
await helpers.configure_reporting(
remote.entity_id, cluster, 0, min_report=1, max_report=600,
reportable_change=1
)
return remote
class BinarySensor(ZhaEntity, BinarySensorDevice):
"""The ZHA Binary Sensor."""
_domain = DOMAIN _domain = DOMAIN
@ -119,11 +104,6 @@ class BinarySensor(ZhaEntity, BinarySensorDevice):
from zigpy.zcl.clusters.security import IasZone from zigpy.zcl.clusters.security import IasZone
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id] self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
@property
def should_poll(self) -> bool:
"""Let zha handle polling."""
return False
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return True if entity is on.""" """Return True if entity is on."""
@ -147,6 +127,26 @@ class BinarySensor(ZhaEntity, BinarySensorDevice):
res = self._ias_zone_cluster.enroll_response(0, 0) res = self._ias_zone_cluster.enroll_response(0, 0)
self.hass.async_add_job(res) self.hass.async_add_job(res)
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
await super().async_added_to_hass()
old_state = await self.async_get_last_state()
if self._state is not None or old_state is None:
return
_LOGGER.debug("%s restoring old state: %s", self.entity_id, old_state)
if old_state.state == STATE_ON:
self._state = 3
else:
self._state = 0
async def async_configure(self):
"""Configure IAS device."""
await self._ias_zone_cluster.bind()
ieee = self._ias_zone_cluster.endpoint.device.application.ieee
await self._ias_zone_cluster.write_attributes({'cie_addr': ieee})
_LOGGER.debug("%s: finished configuration", self.entity_id)
async def async_update(self): async def async_update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
from zigpy.types.basic import uint16_t from zigpy.types.basic import uint16_t
@ -160,81 +160,33 @@ class BinarySensor(ZhaEntity, BinarySensorDevice):
self._state = result.get('zone_status', self._state) & 3 self._state = result.get('zone_status', self._state) & 3
class Remote(ZhaEntity, BinarySensorDevice): class Remote(RestoreEntity, ZhaEntity, BinarySensorDevice):
"""ZHA switch/remote controller/button.""" """ZHA switch/remote controller/button."""
_domain = DOMAIN _domain = DOMAIN
class OnOffListener:
"""Listener for the OnOff Zigbee cluster."""
def __init__(self, entity):
"""Initialize OnOffListener."""
self._entity = entity
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id in (0x0000, 0x0040):
self._entity.set_state(False)
elif command_id in (0x0001, 0x0041, 0x0042):
self._entity.set_state(True)
elif command_id == 0x0002:
self._entity.set_state(not self._entity.is_on)
def attribute_updated(self, attrid, value):
"""Handle attribute updates on this cluster."""
if attrid == 0:
self._entity.set_state(value)
def zdo_command(self, *args, **kwargs):
"""Handle ZDO commands on this cluster."""
pass
class LevelListener:
"""Listener for the LevelControl Zigbee cluster."""
def __init__(self, entity):
"""Initialize LevelListener."""
self._entity = entity
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id in (0x0000, 0x0004): # move_to_level, -with_on_off
self._entity.set_level(args[0])
elif command_id in (0x0001, 0x0005): # move, -with_on_off
# We should dim slowly -- for now, just step once
rate = args[1]
if args[0] == 0xff:
rate = 10 # Should read default move rate
self._entity.move_level(-rate if args[0] else rate)
elif command_id in (0x0002, 0x0006): # step, -with_on_off
# Step (technically may change on/off)
self._entity.move_level(-args[1] if args[0] else args[1])
def attribute_update(self, attrid, value):
"""Handle attribute updates on this cluster."""
if attrid == 0:
self._entity.set_level(value)
def zdo_command(self, *args, **kwargs):
"""Handle ZDO commands on this cluster."""
pass
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Initialize Switch.""" """Initialize Switch."""
super().__init__(**kwargs) super().__init__(**kwargs)
self._state = False
self._level = 0 self._level = 0
from zigpy.zcl.clusters import general from zigpy.zcl.clusters import general
self._out_listeners = { self._out_listeners = {
general.OnOff.cluster_id: self.OnOffListener(self), general.OnOff.cluster_id: OnOffListener(
general.LevelControl.cluster_id: self.LevelListener(self), self,
self._out_clusters[general.OnOff.cluster_id]
)
} }
@property out_clusters = kwargs.get('out_clusters')
def should_poll(self) -> bool: self._zcl_reporting = {}
"""Let zha handle polling."""
return False if general.LevelControl.cluster_id in out_clusters:
self._out_listeners.update({
general.LevelControl.cluster_id: LevelListener(
self,
out_clusters[general.LevelControl.cluster_id]
)
})
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
@ -249,6 +201,11 @@ class Remote(ZhaEntity, BinarySensorDevice):
}) })
return self._device_state_attributes return self._device_state_attributes
@property
def zcl_reporting_config(self):
"""Return ZCL attribute reporting configuration."""
return self._zcl_reporting
def move_level(self, change): def move_level(self, change):
"""Increment the level, setting state if appropriate.""" """Increment the level, setting state if appropriate."""
if not self._state and change > 0: if not self._state and change > 0:
@ -270,6 +227,31 @@ class Remote(ZhaEntity, BinarySensorDevice):
self._level = 255 self._level = 255
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
async def async_configure(self):
"""Bind clusters."""
from zigpy.zcl.clusters import general
await helpers.bind_cluster(
self.entity_id,
self._out_clusters[general.OnOff.cluster_id]
)
if general.LevelControl.cluster_id in self._out_clusters:
await helpers.bind_cluster(
self.entity_id,
self._out_clusters[general.LevelControl.cluster_id]
)
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
await super().async_added_to_hass()
old_state = await self.async_get_last_state()
if self._state is not None or old_state is None:
return
_LOGGER.debug("%s restoring old state: %s", self.entity_id, old_state)
if 'level' in old_state.attributes:
self._level = old_state.attributes['level']
self._state = old_state.state == STATE_ON
async def async_update(self): async def async_update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.general import OnOff
@ -280,3 +262,56 @@ class Remote(ZhaEntity, BinarySensorDevice):
only_cache=(not self._initialized) only_cache=(not self._initialized)
) )
self._state = result.get('on_off', self._state) self._state = result.get('on_off', self._state)
class BinarySensor(RestoreEntity, ZhaEntity, BinarySensorDevice):
"""ZHA switch."""
_domain = DOMAIN
_device_class = None
value_attribute = 0
def __init__(self, device_class, **kwargs):
"""Initialize the ZHA binary sensor."""
super().__init__(**kwargs)
self._device_class = device_class
self._cluster = list(kwargs['in_clusters'].values())[0]
def attribute_updated(self, attribute, value):
"""Handle attribute update from device."""
_LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value)
if attribute == self.value_attribute:
self._state = bool(value)
self.async_schedule_update_ha_state()
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
await super().async_added_to_hass()
old_state = await self.async_get_last_state()
if self._state is not None or old_state is None:
return
_LOGGER.debug("%s restoring old state: %s", self.entity_id, old_state)
self._state = old_state.state == STATE_ON
@property
def cluster(self):
"""Zigbee cluster for this entity."""
return self._cluster
@property
def zcl_reporting_config(self):
"""ZHA reporting configuration."""
return {self.cluster: {self.value_attribute: REPORT_CONFIG_IMMEDIATE}}
@property
def is_on(self) -> bool:
"""Return if the switch is on based on the statemachine."""
if self._state is None:
return False
return self._state
@property
def device_class(self) -> str:
"""Return device class from component DEVICE_CLASSES."""
return self._device_class

View File

@ -61,7 +61,7 @@ FALLBACK_STREAM_INTERVAL = 1 # seconds
MIN_STREAM_INTERVAL = 0.5 # seconds MIN_STREAM_INTERVAL = 0.5 # seconds
CAMERA_SERVICE_SCHEMA = vol.Schema({ CAMERA_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
}) })
CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({ CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({

View File

@ -7,7 +7,7 @@ https://home-assistant.io/components/camera.axis/
import logging import logging
from homeassistant.components.camera.mjpeg import ( from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera) CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera, filter_urllib3_logging)
from homeassistant.const import ( from homeassistant.const import (
CONF_AUTHENTICATION, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_AUTHENTICATION, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT,
CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION) CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION)
@ -29,6 +29,8 @@ def _get_image_url(host, port, mode):
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Axis camera.""" """Set up the Axis camera."""
filter_urllib3_logging()
camera_config = { camera_config = {
CONF_NAME: discovery_info[CONF_NAME], CONF_NAME: discovery_info[CONF_NAME],
CONF_USERNAME: discovery_info[CONF_USERNAME], CONF_USERNAME: discovery_info[CONF_USERNAME],

View File

@ -16,7 +16,7 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, CONF_VERIFY_SSL)
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_web) async_get_clientsession, async_aiohttp_proxy_web)
@ -29,6 +29,7 @@ CONF_STILL_IMAGE_URL = 'still_image_url'
CONTENT_TYPE_HEADER = 'Content-Type' CONTENT_TYPE_HEADER = 'Content-Type'
DEFAULT_NAME = 'Mjpeg Camera' DEFAULT_NAME = 'Mjpeg Camera'
DEFAULT_VERIFY_SSL = True
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_MJPEG_URL): cv.url, vol.Required(CONF_MJPEG_URL): cv.url,
@ -38,13 +39,22 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
}) })
async def async_setup_platform(hass, config, async_add_entities, async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None): discovery_info=None):
"""Set up a MJPEG IP Camera.""" """Set up a MJPEG IP Camera."""
# Filter header errors from urllib3 due to a urllib3 bug filter_urllib3_logging()
if discovery_info:
config = PLATFORM_SCHEMA(discovery_info)
async_add_entities([MjpegCamera(config)])
def filter_urllib3_logging():
"""Filter header errors from urllib3 due to a urllib3 bug."""
urllib3_logger = logging.getLogger("urllib3.connectionpool") urllib3_logger = logging.getLogger("urllib3.connectionpool")
if not any(isinstance(x, NoHeaderErrorFilter) if not any(isinstance(x, NoHeaderErrorFilter)
for x in urllib3_logger.filters): for x in urllib3_logger.filters):
@ -52,10 +62,6 @@ async def async_setup_platform(hass, config, async_add_entities,
NoHeaderErrorFilter() NoHeaderErrorFilter()
) )
if discovery_info:
config = PLATFORM_SCHEMA(discovery_info)
async_add_entities([MjpegCamera(config)])
def extract_image_from_mjpeg(stream): def extract_image_from_mjpeg(stream):
"""Take in a MJPEG stream object, return the jpg from it.""" """Take in a MJPEG stream object, return the jpg from it."""
@ -95,6 +101,7 @@ class MjpegCamera(Camera):
self._auth = aiohttp.BasicAuth( self._auth = aiohttp.BasicAuth(
self._username, password=self._password self._username, password=self._password
) )
self._verify_ssl = device_info.get(CONF_VERIFY_SSL)
async def async_camera_image(self): async def async_camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
@ -105,7 +112,10 @@ class MjpegCamera(Camera):
self.camera_image) self.camera_image)
return image return image
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(
self.hass,
verify_ssl=self._verify_ssl
)
try: try:
with async_timeout.timeout(10, loop=self.hass.loop): with async_timeout.timeout(10, loop=self.hass.loop):
response = await websession.get( response = await websession.get(
@ -128,7 +138,12 @@ class MjpegCamera(Camera):
else: else:
auth = HTTPBasicAuth(self._username, self._password) auth = HTTPBasicAuth(self._username, self._password)
req = requests.get( req = requests.get(
self._mjpeg_url, auth=auth, stream=True, timeout=10) self._mjpeg_url,
auth=auth,
stream=True,
timeout=10,
verify=self._verify_ssl
)
else: else:
req = requests.get(self._mjpeg_url, stream=True, timeout=10) req = requests.get(self._mjpeg_url, stream=True, timeout=10)
@ -144,7 +159,10 @@ class MjpegCamera(Camera):
return await super().handle_async_mjpeg_stream(request) return await super().handle_async_mjpeg_stream(request)
# connect to stream # connect to stream
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(
self.hass,
verify_ssl=self._verify_ssl
)
stream_coro = websession.get(self._mjpeg_url, auth=self._auth) stream_coro = websession.get(self._mjpeg_url, auth=self._auth)
return await async_aiohttp_proxy_web(self.hass, request, stream_coro) return await async_aiohttp_proxy_web(self.hass, request, stream_coro)

View File

@ -8,6 +8,11 @@ from datetime import timedelta
import logging import logging
import requests import requests
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA
from homeassistant.const import CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from homeassistant.components.skybell import ( from homeassistant.components.skybell import (
@ -19,14 +24,33 @@ _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=90) SCAN_INTERVAL = timedelta(seconds=90)
IMAGE_AVATAR = 'avatar'
IMAGE_ACTIVITY = 'activity'
CONF_ACTIVITY_NAME = 'activity_name'
CONF_AVATAR_NAME = 'avatar_name'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MONITORED_CONDITIONS, default=[IMAGE_AVATAR]):
vol.All(cv.ensure_list, [vol.In([IMAGE_AVATAR, IMAGE_ACTIVITY])]),
vol.Optional(CONF_ACTIVITY_NAME): cv.string,
vol.Optional(CONF_AVATAR_NAME): cv.string,
})
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the platform for a Skybell device.""" """Set up the platform for a Skybell device."""
cond = config[CONF_MONITORED_CONDITIONS]
names = {}
names[IMAGE_ACTIVITY] = config.get(CONF_ACTIVITY_NAME)
names[IMAGE_AVATAR] = config.get(CONF_AVATAR_NAME)
skybell = hass.data.get(SKYBELL_DOMAIN) skybell = hass.data.get(SKYBELL_DOMAIN)
sensors = [] sensors = []
for device in skybell.get_devices(): for device in skybell.get_devices():
sensors.append(SkybellCamera(device)) for camera_type in cond:
sensors.append(SkybellCamera(device, camera_type,
names.get(camera_type)))
add_entities(sensors, True) add_entities(sensors, True)
@ -34,11 +58,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class SkybellCamera(SkybellDevice, Camera): class SkybellCamera(SkybellDevice, Camera):
"""A camera implementation for Skybell devices.""" """A camera implementation for Skybell devices."""
def __init__(self, device): def __init__(self, device, camera_type, name=None):
"""Initialize a camera for a Skybell device.""" """Initialize a camera for a Skybell device."""
self._type = camera_type
SkybellDevice.__init__(self, device) SkybellDevice.__init__(self, device)
Camera.__init__(self) Camera.__init__(self)
self._name = self._device.name if name is not None:
self._name = "{} {}".format(self._device.name, name)
else:
self._name = self._device.name
self._url = None self._url = None
self._response = None self._response = None
@ -47,12 +75,19 @@ class SkybellCamera(SkybellDevice, Camera):
"""Return the name of the sensor.""" """Return the name of the sensor."""
return self._name return self._name
@property
def image_url(self):
"""Get the camera image url based on type."""
if self._type == IMAGE_ACTIVITY:
return self._device.activity_image
return self._device.image
def camera_image(self): def camera_image(self):
"""Get the latest camera image.""" """Get the latest camera image."""
super().update() super().update()
if self._url != self._device.image: if self._url != self.image_url:
self._url = self._device.image self._url = self.image_url
try: try:
self._response = requests.get( self._response = requests.get(

View File

@ -17,7 +17,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
REQUIREMENTS = ['aioftp==0.10.1'] REQUIREMENTS = ['aioftp==0.12.0']
DEPENDENCIES = ['ffmpeg'] DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -6,9 +6,9 @@ https://home-assistant.io/components/camera.zoneminder/
""" """
import logging import logging
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME, CONF_VERIFY_SSL
from homeassistant.components.camera.mjpeg import ( from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera) CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera, filter_urllib3_logging)
from homeassistant.components.zoneminder import DOMAIN as ZONEMINDER_DOMAIN from homeassistant.components.zoneminder import DOMAIN as ZONEMINDER_DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -18,6 +18,7 @@ DEPENDENCIES = ['zoneminder']
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ZoneMinder cameras.""" """Set up the ZoneMinder cameras."""
filter_urllib3_logging()
zm_client = hass.data[ZONEMINDER_DOMAIN] zm_client = hass.data[ZONEMINDER_DOMAIN]
monitors = zm_client.get_monitors() monitors = zm_client.get_monitors()
@ -28,22 +29,24 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
cameras = [] cameras = []
for monitor in monitors: for monitor in monitors:
_LOGGER.info("Initializing camera %s", monitor.id) _LOGGER.info("Initializing camera %s", monitor.id)
cameras.append(ZoneMinderCamera(monitor)) cameras.append(ZoneMinderCamera(monitor, zm_client.verify_ssl))
add_entities(cameras) add_entities(cameras)
class ZoneMinderCamera(MjpegCamera): class ZoneMinderCamera(MjpegCamera):
"""Representation of a ZoneMinder Monitor Stream.""" """Representation of a ZoneMinder Monitor Stream."""
def __init__(self, monitor): def __init__(self, monitor, verify_ssl):
"""Initialize as a subclass of MjpegCamera.""" """Initialize as a subclass of MjpegCamera."""
device_info = { device_info = {
CONF_NAME: monitor.name, CONF_NAME: monitor.name,
CONF_MJPEG_URL: monitor.mjpeg_image_url, CONF_MJPEG_URL: monitor.mjpeg_image_url,
CONF_STILL_IMAGE_URL: monitor.still_image_url CONF_STILL_IMAGE_URL: monitor.still_image_url,
CONF_VERIFY_SSL: verify_ssl
} }
super().__init__(device_info) super().__init__(device_info)
self._is_recording = None self._is_recording = None
self._is_available = None
self._monitor = monitor self._monitor = monitor
@property @property
@ -55,8 +58,14 @@ class ZoneMinderCamera(MjpegCamera):
"""Update our recording state from the ZM API.""" """Update our recording state from the ZM API."""
_LOGGER.debug("Updating camera state for monitor %i", self._monitor.id) _LOGGER.debug("Updating camera state for monitor %i", self._monitor.id)
self._is_recording = self._monitor.is_recording self._is_recording = self._monitor.is_recording
self._is_available = self._monitor.is_available
@property @property
def is_recording(self): def is_recording(self):
"""Return whether the monitor is in alarm mode.""" """Return whether the monitor is in alarm mode."""
return self._is_recording return self._is_recording
@property
def available(self):
"""Return True if entity is available."""
return self._is_available

View File

@ -6,7 +6,7 @@
}, },
"step": { "step": {
"confirm": { "confirm": {
"description": "Voleu configurar Google Cast?", "description": "Vols configurar Google Cast?",
"title": "Google Cast" "title": "Google Cast"
} }
}, },

View File

@ -1,7 +1,8 @@
{ {
"config": { "config": {
"abort": { "abort": {
"no_devices_found": "Nem tal\u00e1lhat\u00f3k Google Cast eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton." "no_devices_found": "Nem tal\u00e1lhat\u00f3k Google Cast eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton.",
"single_instance_allowed": "Csak egyetlen Google Cast konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges."
}, },
"step": { "step": {
"confirm": { "confirm": {

View File

@ -92,15 +92,15 @@ CONVERTIBLE_ATTRIBUTE = [
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ON_OFF_SERVICE_SCHEMA = vol.Schema({ ON_OFF_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
}) })
SET_AWAY_MODE_SCHEMA = vol.Schema({ SET_AWAY_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
vol.Required(ATTR_AWAY_MODE): cv.boolean, vol.Required(ATTR_AWAY_MODE): cv.boolean,
}) })
SET_AUX_HEAT_SCHEMA = vol.Schema({ SET_AUX_HEAT_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
vol.Required(ATTR_AUX_HEAT): cv.boolean, vol.Required(ATTR_AUX_HEAT): cv.boolean,
}) })
SET_TEMPERATURE_SCHEMA = vol.Schema(vol.All( SET_TEMPERATURE_SCHEMA = vol.Schema(vol.All(
@ -110,28 +110,28 @@ SET_TEMPERATURE_SCHEMA = vol.Schema(vol.All(
vol.Exclusive(ATTR_TEMPERATURE, 'temperature'): vol.Coerce(float), vol.Exclusive(ATTR_TEMPERATURE, 'temperature'): vol.Coerce(float),
vol.Inclusive(ATTR_TARGET_TEMP_HIGH, 'temperature'): vol.Coerce(float), vol.Inclusive(ATTR_TARGET_TEMP_HIGH, 'temperature'): vol.Coerce(float),
vol.Inclusive(ATTR_TARGET_TEMP_LOW, 'temperature'): vol.Coerce(float), vol.Inclusive(ATTR_TARGET_TEMP_LOW, 'temperature'): vol.Coerce(float),
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
vol.Optional(ATTR_OPERATION_MODE): cv.string, vol.Optional(ATTR_OPERATION_MODE): cv.string,
} }
)) ))
SET_FAN_MODE_SCHEMA = vol.Schema({ SET_FAN_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
vol.Required(ATTR_FAN_MODE): cv.string, vol.Required(ATTR_FAN_MODE): cv.string,
}) })
SET_HOLD_MODE_SCHEMA = vol.Schema({ SET_HOLD_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
vol.Required(ATTR_HOLD_MODE): cv.string, vol.Required(ATTR_HOLD_MODE): cv.string,
}) })
SET_OPERATION_MODE_SCHEMA = vol.Schema({ SET_OPERATION_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
vol.Required(ATTR_OPERATION_MODE): cv.string, vol.Required(ATTR_OPERATION_MODE): cv.string,
}) })
SET_HUMIDITY_SCHEMA = vol.Schema({ SET_HUMIDITY_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
vol.Required(ATTR_HUMIDITY): vol.Coerce(float), vol.Required(ATTR_HUMIDITY): vol.Coerce(float),
}) })
SET_SWING_MODE_SCHEMA = vol.Schema({ SET_SWING_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
vol.Required(ATTR_SWING_MODE): cv.string, vol.Required(ATTR_SWING_MODE): cv.string,
}) })

View File

@ -15,14 +15,13 @@ from homeassistant.components.climate import (
STATE_FAN_ONLY, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE, STATE_FAN_ONLY, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE,
ClimateDevice) ClimateDevice)
from homeassistant.components.daikin import ( from homeassistant.components.daikin import DOMAIN as DAIKIN_DOMAIN
ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, ATTR_TARGET_TEMPERATURE, from homeassistant.components.daikin.const import (
daikin_api_setup) ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, ATTR_TARGET_TEMPERATURE)
from homeassistant.const import ( from homeassistant.const import (
ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS) ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pydaikin==0.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -60,18 +59,18 @@ HA_ATTR_TO_DAIKIN = {
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Daikin HVAC platform.""" """Old way of setting up the Daikin HVAC platform.
if discovery_info is not None:
host = discovery_info.get('ip')
name = None
_LOGGER.debug("Discovered a Daikin AC on %s", host)
else:
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
_LOGGER.debug("Added Daikin AC on %s", host)
api = daikin_api_setup(hass, host, name) Can only be called when a user accidentally mentions the platform in their
add_entities([DaikinClimate(api)], True) config. But even in that case it would have been ignored.
"""
pass
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Daikin climate based on config_entry."""
daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id)
async_add_entities([DaikinClimate(daikin_api)])
class DaikinClimate(ClimateDevice): class DaikinClimate(ClimateDevice):
@ -266,3 +265,8 @@ class DaikinClimate(ClimateDevice):
def update(self): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
self._api.update() self._api.update()
@property
def device_info(self):
"""Return a device description for device registry."""
return self._api.device_info

View File

@ -9,7 +9,8 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice, STATE_ON, STATE_OFF, STATE_HEAT, STATE_MANUAL, STATE_ECO, PLATFORM_SCHEMA,
ClimateDevice,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE,
SUPPORT_ON_OFF) SUPPORT_ON_OFF)
from homeassistant.const import ( from homeassistant.const import (
@ -21,8 +22,6 @@ REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.45']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
STATE_BOOST = 'boost' STATE_BOOST = 'boost'
STATE_AWAY = 'away'
STATE_MANUAL = 'manual'
ATTR_STATE_WINDOW_OPEN = 'window_open' ATTR_STATE_WINDOW_OPEN = 'window_open'
ATTR_STATE_VALVE = 'valve' ATTR_STATE_VALVE = 'valve'
@ -65,10 +64,10 @@ class EQ3BTSmartThermostat(ClimateDevice):
self.modes = { self.modes = {
eq3.Mode.Open: STATE_ON, eq3.Mode.Open: STATE_ON,
eq3.Mode.Closed: STATE_OFF, eq3.Mode.Closed: STATE_OFF,
eq3.Mode.Auto: STATE_AUTO, eq3.Mode.Auto: STATE_HEAT,
eq3.Mode.Manual: STATE_MANUAL, eq3.Mode.Manual: STATE_MANUAL,
eq3.Mode.Boost: STATE_BOOST, eq3.Mode.Boost: STATE_BOOST,
eq3.Mode.Away: STATE_AWAY, eq3.Mode.Away: STATE_ECO,
} }
self.reverse_modes = {v: k for k, v in self.modes.items()} self.reverse_modes = {v: k for k, v in self.modes.items()}
@ -140,20 +139,20 @@ class EQ3BTSmartThermostat(ClimateDevice):
def turn_away_mode_off(self): def turn_away_mode_off(self):
"""Away mode off turns to AUTO mode.""" """Away mode off turns to AUTO mode."""
self.set_operation_mode(STATE_AUTO) self.set_operation_mode(STATE_HEAT)
def turn_away_mode_on(self): def turn_away_mode_on(self):
"""Set away mode on.""" """Set away mode on."""
self.set_operation_mode(STATE_AWAY) self.set_operation_mode(STATE_ECO)
@property @property
def is_away_mode_on(self): def is_away_mode_on(self):
"""Return if we are away.""" """Return if we are away."""
return self.current_operation == STATE_AWAY return self.current_operation == STATE_ECO
def turn_on(self): def turn_on(self):
"""Turn device on.""" """Turn device on."""
self.set_operation_mode(STATE_AUTO) self.set_operation_mode(STATE_HEAT)
def turn_off(self): def turn_off(self):
"""Turn device off.""" """Turn device off."""

View File

@ -50,23 +50,23 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
def update_characteristics(self, characteristics): def update_characteristics(self, characteristics):
"""Synchronise device state with Home Assistant.""" """Synchronise device state with Home Assistant."""
# pylint: disable=import-error # pylint: disable=import-error
from homekit import CharacteristicsTypes as ctypes from homekit.models.characteristics import CharacteristicsTypes
for characteristic in characteristics: for characteristic in characteristics:
ctype = characteristic['type'] ctype = characteristic['type']
if ctype == ctypes.HEATING_COOLING_CURRENT: if ctype == CharacteristicsTypes.HEATING_COOLING_CURRENT:
self._state = MODE_HOMEKIT_TO_HASS.get( self._state = MODE_HOMEKIT_TO_HASS.get(
characteristic['value']) characteristic['value'])
if ctype == ctypes.HEATING_COOLING_TARGET: if ctype == CharacteristicsTypes.HEATING_COOLING_TARGET:
self._chars['target_mode'] = characteristic['iid'] self._chars['target_mode'] = characteristic['iid']
self._features |= SUPPORT_OPERATION_MODE self._features |= SUPPORT_OPERATION_MODE
self._current_mode = MODE_HOMEKIT_TO_HASS.get( self._current_mode = MODE_HOMEKIT_TO_HASS.get(
characteristic['value']) characteristic['value'])
self._valid_modes = [MODE_HOMEKIT_TO_HASS.get( self._valid_modes = [MODE_HOMEKIT_TO_HASS.get(
mode) for mode in characteristic['valid-values']] mode) for mode in characteristic['valid-values']]
elif ctype == ctypes.TEMPERATURE_CURRENT: elif ctype == CharacteristicsTypes.TEMPERATURE_CURRENT:
self._current_temp = characteristic['value'] self._current_temp = characteristic['value']
elif ctype == ctypes.TEMPERATURE_TARGET: elif ctype == CharacteristicsTypes.TEMPERATURE_TARGET:
self._chars['target_temp'] = characteristic['iid'] self._chars['target_temp'] = characteristic['iid']
self._features |= SUPPORT_TARGET_TEMPERATURE self._features |= SUPPORT_TARGET_TEMPERATURE
self._target_temp = characteristic['value'] self._target_temp = characteristic['value']

View File

@ -6,14 +6,17 @@ https://home-assistant.io/components/climate.knx/
""" """
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import (
PLATFORM_SCHEMA, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
ClimateDevice)
from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX
from homeassistant.const import ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.climate import (
PLATFORM_SCHEMA, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_TEMPERATURE, STATE_HEAT,
STATE_IDLE, STATE_MANUAL, STATE_DRY,
STATE_FAN_ONLY, STATE_ECO, ClimateDevice)
from homeassistant.const import (
ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS)
from homeassistant.core import callback
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
CONF_SETPOINT_SHIFT_ADDRESS = 'setpoint_shift_address' CONF_SETPOINT_SHIFT_ADDRESS = 'setpoint_shift_address'
CONF_SETPOINT_SHIFT_STATE_ADDRESS = 'setpoint_shift_state_address' CONF_SETPOINT_SHIFT_STATE_ADDRESS = 'setpoint_shift_state_address'
@ -26,10 +29,17 @@ CONF_OPERATION_MODE_ADDRESS = 'operation_mode_address'
CONF_OPERATION_MODE_STATE_ADDRESS = 'operation_mode_state_address' CONF_OPERATION_MODE_STATE_ADDRESS = 'operation_mode_state_address'
CONF_CONTROLLER_STATUS_ADDRESS = 'controller_status_address' CONF_CONTROLLER_STATUS_ADDRESS = 'controller_status_address'
CONF_CONTROLLER_STATUS_STATE_ADDRESS = 'controller_status_state_address' CONF_CONTROLLER_STATUS_STATE_ADDRESS = 'controller_status_state_address'
CONF_CONTROLLER_MODE_ADDRESS = 'controller_mode_address'
CONF_CONTROLLER_MODE_STATE_ADDRESS = 'controller_mode_state_address'
CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = \ CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = \
'operation_mode_frost_protection_address' 'operation_mode_frost_protection_address'
CONF_OPERATION_MODE_NIGHT_ADDRESS = 'operation_mode_night_address' CONF_OPERATION_MODE_NIGHT_ADDRESS = 'operation_mode_night_address'
CONF_OPERATION_MODE_COMFORT_ADDRESS = 'operation_mode_comfort_address' CONF_OPERATION_MODE_COMFORT_ADDRESS = 'operation_mode_comfort_address'
CONF_OPERATION_MODES = 'operation_modes'
CONF_ON_OFF_ADDRESS = 'on_off_address'
CONF_ON_OFF_STATE_ADDRESS = 'on_off_state_address'
CONF_MIN_TEMP = 'min_temp'
CONF_MAX_TEMP = 'max_temp'
DEFAULT_NAME = 'KNX Climate' DEFAULT_NAME = 'KNX Climate'
DEFAULT_SETPOINT_SHIFT_STEP = 0.5 DEFAULT_SETPOINT_SHIFT_STEP = 0.5
@ -37,6 +47,21 @@ DEFAULT_SETPOINT_SHIFT_MAX = 6
DEFAULT_SETPOINT_SHIFT_MIN = -6 DEFAULT_SETPOINT_SHIFT_MIN = -6
DEPENDENCIES = ['knx'] DEPENDENCIES = ['knx']
# Map KNX operation modes to HA modes. This list might not be full.
OPERATION_MODES = {
# Map DPT 201.100 HVAC operating modes
"Frost Protection": STATE_MANUAL,
"Night": STATE_IDLE,
"Standby": STATE_ECO,
"Comfort": STATE_HEAT,
# Map DPT 201.104 HVAC control modes
"Fan only": STATE_FAN_ONLY,
"Dehumidification": STATE_DRY
}
OPERATION_MODES_INV = dict((
reversed(item) for item in OPERATION_MODES.items()))
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string, vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string,
@ -54,9 +79,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string, vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string,
vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string, vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string,
vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): cv.string, vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): cv.string,
vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): cv.string,
vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): cv.string,
vol.Optional(CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS): cv.string, vol.Optional(CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS): cv.string,
vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): cv.string, vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): cv.string,
vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string, vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string,
vol.Optional(CONF_ON_OFF_ADDRESS): cv.string,
vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string,
vol.Optional(CONF_OPERATION_MODES): vol.All(cv.ensure_list,
[vol.In(OPERATION_MODES)]),
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
}) })
@ -84,6 +117,30 @@ def async_add_entities_config(hass, config, async_add_entities):
"""Set up climate for KNX platform configured within platform.""" """Set up climate for KNX platform configured within platform."""
import xknx import xknx
climate_mode = xknx.devices.ClimateMode(
hass.data[DATA_KNX].xknx,
name=config.get(CONF_NAME) + " Mode",
group_address_operation_mode=config.get(CONF_OPERATION_MODE_ADDRESS),
group_address_operation_mode_state=config.get(
CONF_OPERATION_MODE_STATE_ADDRESS),
group_address_controller_status=config.get(
CONF_CONTROLLER_STATUS_ADDRESS),
group_address_controller_status_state=config.get(
CONF_CONTROLLER_STATUS_STATE_ADDRESS),
group_address_controller_mode=config.get(
CONF_CONTROLLER_MODE_ADDRESS),
group_address_controller_mode_state=config.get(
CONF_CONTROLLER_MODE_STATE_ADDRESS),
group_address_operation_mode_protection=config.get(
CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS),
group_address_operation_mode_night=config.get(
CONF_OPERATION_MODE_NIGHT_ADDRESS),
group_address_operation_mode_comfort=config.get(
CONF_OPERATION_MODE_COMFORT_ADDRESS),
operation_modes=config.get(
CONF_OPERATION_MODES))
hass.data[DATA_KNX].xknx.devices.add(climate_mode)
climate = xknx.devices.Climate( climate = xknx.devices.Climate(
hass.data[DATA_KNX].xknx, hass.data[DATA_KNX].xknx,
name=config.get(CONF_NAME), name=config.get(CONF_NAME),
@ -96,20 +153,15 @@ def async_add_entities_config(hass, config, async_add_entities):
setpoint_shift_step=config.get(CONF_SETPOINT_SHIFT_STEP), setpoint_shift_step=config.get(CONF_SETPOINT_SHIFT_STEP),
setpoint_shift_max=config.get(CONF_SETPOINT_SHIFT_MAX), setpoint_shift_max=config.get(CONF_SETPOINT_SHIFT_MAX),
setpoint_shift_min=config.get(CONF_SETPOINT_SHIFT_MIN), setpoint_shift_min=config.get(CONF_SETPOINT_SHIFT_MIN),
group_address_operation_mode=config.get(CONF_OPERATION_MODE_ADDRESS), group_address_on_off=config.get(
group_address_operation_mode_state=config.get( CONF_ON_OFF_ADDRESS),
CONF_OPERATION_MODE_STATE_ADDRESS), group_address_on_off_state=config.get(
group_address_controller_status=config.get( CONF_ON_OFF_STATE_ADDRESS),
CONF_CONTROLLER_STATUS_ADDRESS), min_temp=config.get(CONF_MIN_TEMP),
group_address_controller_status_state=config.get( max_temp=config.get(CONF_MAX_TEMP),
CONF_CONTROLLER_STATUS_STATE_ADDRESS), mode=climate_mode)
group_address_operation_mode_protection=config.get(
CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS),
group_address_operation_mode_night=config.get(
CONF_OPERATION_MODE_NIGHT_ADDRESS),
group_address_operation_mode_comfort=config.get(
CONF_OPERATION_MODE_COMFORT_ADDRESS))
hass.data[DATA_KNX].xknx.devices.add(climate) hass.data[DATA_KNX].xknx.devices.add(climate)
async_add_entities([KNXClimate(climate)]) async_add_entities([KNXClimate(climate)])
@ -119,26 +171,25 @@ class KNXClimate(ClimateDevice):
def __init__(self, device): def __init__(self, device):
"""Initialize of a KNX climate device.""" """Initialize of a KNX climate device."""
self.device = device self.device = device
self._unit_of_measurement = TEMP_CELSIUS
@property @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
support = SUPPORT_TARGET_TEMPERATURE support = SUPPORT_TARGET_TEMPERATURE
if self.device.supports_operation_mode: if self.device.mode.supports_operation_mode:
support |= SUPPORT_OPERATION_MODE support |= SUPPORT_OPERATION_MODE
if self.device.supports_on_off:
support |= SUPPORT_ON_OFF
return support return support
def async_register_callbacks(self): async def async_added_to_hass(self):
"""Register callbacks to update hass after device was changed.""" """Register callbacks to update hass after device was changed."""
async def after_update_callback(device): async def after_update_callback(device):
"""Call after device was updated.""" """Call after device was updated."""
await self.async_update_ha_state() await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback) self.device.register_device_updated_cb(after_update_callback)
async def async_added_to_hass(self):
"""Store register state change callback."""
self.async_register_callbacks()
@property @property
def name(self): def name(self):
"""Return the name of the KNX device.""" """Return the name of the KNX device."""
@ -157,7 +208,7 @@ class KNXClimate(ClimateDevice):
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
return TEMP_CELSIUS return self._unit_of_measurement
@property @property
def current_temperature(self): def current_temperature(self):
@ -195,20 +246,37 @@ class KNXClimate(ClimateDevice):
@property @property
def current_operation(self): def current_operation(self):
"""Return current operation ie. heat, cool, idle.""" """Return current operation ie. heat, cool, idle."""
if self.device.supports_operation_mode: if self.device.mode.supports_operation_mode:
return self.device.operation_mode.value return OPERATION_MODES.get(self.device.mode.operation_mode.value)
return None return None
@property @property
def operation_list(self): def operation_list(self):
"""Return the list of available operation modes.""" """Return the list of available operation modes."""
return [operation_mode.value for return [OPERATION_MODES.get(operation_mode.value) for
operation_mode in operation_mode in
self.device.get_supported_operation_modes()] self.device.mode.operation_modes]
async def async_set_operation_mode(self, operation_mode): async def async_set_operation_mode(self, operation_mode):
"""Set operation mode.""" """Set operation mode."""
if self.device.supports_operation_mode: if self.device.mode.supports_operation_mode:
from xknx.knx import HVACOperationMode from xknx.knx import HVACOperationMode
knx_operation_mode = HVACOperationMode(operation_mode) knx_operation_mode = HVACOperationMode(
await self.device.set_operation_mode(knx_operation_mode) OPERATION_MODES_INV.get(operation_mode))
await self.device.mode.set_operation_mode(knx_operation_mode)
await self.async_update_ha_state()
@property
def is_on(self):
"""Return true if the device is on."""
if self.device.supports_on_off:
return self.device.is_on
return None
async def async_turn_on(self):
"""Turn on."""
await self.device.turn_on()
async def async_turn_off(self):
"""Turn off."""
await self.device.turn_off()

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
REQUIREMENTS = ['millheater==0.2.9'] REQUIREMENTS = ['millheater==0.3.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -18,12 +18,13 @@ from homeassistant.components.climate import (
SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE,
SUPPORT_AUX_HEAT, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) SUPPORT_AUX_HEAT, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, CONF_VALUE_TEMPLATE) ATTR_TEMPERATURE, CONF_DEVICE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_ON,
STATE_OFF)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate, MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate,
subscription) MqttEntityDeviceInfo, subscription)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -78,6 +79,8 @@ CONF_MIN_TEMP = 'min_temp'
CONF_MAX_TEMP = 'max_temp' CONF_MAX_TEMP = 'max_temp'
CONF_TEMP_STEP = 'temp_step' CONF_TEMP_STEP = 'temp_step'
CONF_UNIQUE_ID = 'unique_id'
TEMPLATE_KEYS = ( TEMPLATE_KEYS = (
CONF_POWER_STATE_TEMPLATE, CONF_POWER_STATE_TEMPLATE,
CONF_MODE_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE,
@ -139,8 +142,9 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float),
vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float) vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float),
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@ -174,12 +178,14 @@ async def _async_setup_entity(hass, config, async_add_entities,
)]) )])
class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
ClimateDevice):
"""Representation of an MQTT climate device.""" """Representation of an MQTT climate device."""
def __init__(self, hass, config, discovery_hash): def __init__(self, hass, config, discovery_hash):
"""Initialize the climate device.""" """Initialize the climate device."""
self._config = config self._config = config
self._unique_id = config.get(CONF_UNIQUE_ID)
self._sub_state = None self._sub_state = None
self.hass = hass self.hass = hass
@ -201,11 +207,13 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
payload_available = config.get(CONF_PAYLOAD_AVAILABLE) payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
qos = config.get(CONF_QOS) qos = config.get(CONF_QOS)
device_config = config.get(CONF_DEVICE)
MqttAvailability.__init__(self, availability_topic, qos, MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available) payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash, MqttDiscoveryUpdate.__init__(self, discovery_hash,
self.discovery_update) self.discovery_update)
MqttEntityDeviceInfo.__init__(self, device_config)
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Handle being added to home assistant.""" """Handle being added to home assistant."""
@ -453,7 +461,8 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self):
"""Unsubscribe when removed.""" """Unsubscribe when removed."""
await subscription.async_unsubscribe_topics(self.hass, self._sub_state) self._sub_state = await subscription.async_unsubscribe_topics(
self.hass, self._sub_state)
await MqttAvailability.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self)
@property @property
@ -466,6 +475,11 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
"""Return the name of the climate device.""" """Return the name of the climate device."""
return self._config.get(CONF_NAME) return self._config.get(CONF_NAME)
@property
def unique_id(self):
"""Return a unique ID."""
return self._unique_id
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""

View File

@ -17,7 +17,7 @@ from homeassistant.const import (
CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, PRECISION_HALVES) CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, PRECISION_HALVES)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['radiotherm==1.4.1'] REQUIREMENTS = ['radiotherm==2.0.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -235,13 +235,15 @@ class RadioThermostat(ClimateDevice):
self._name = self.device.name['raw'] self._name = self.device.name['raw']
# Request the current state from the thermostat. # Request the current state from the thermostat.
data = self.device.tstat['raw'] import radiotherm
try:
data = self.device.tstat['raw']
except radiotherm.validate.RadiothermTstatError:
_LOGGER.error('%s (%s) was busy (invalid value returned)',
self._name, self.device.host)
return
current_temp = data['temp'] current_temp = data['temp']
if current_temp == -1:
_LOGGER.error('%s (%s) was busy (temp == -1)', self._name,
self.device.host)
return
# Map thermostat values into various STATE_ flags. # Map thermostat values into various STATE_ flags.
self._current_temperature = current_temp self._current_temperature = current_temp

View File

@ -99,6 +99,8 @@ async def async_setup(hass, config):
kwargs[CONF_GOOGLE_ACTIONS] = GACTIONS_SCHEMA({}) kwargs[CONF_GOOGLE_ACTIONS] = GACTIONS_SCHEMA({})
kwargs[CONF_ALEXA] = alexa_sh.Config( kwargs[CONF_ALEXA] = alexa_sh.Config(
endpoint=None,
async_get_access_token=None,
should_expose=alexa_conf[CONF_FILTER], should_expose=alexa_conf[CONF_FILTER],
entity_config=alexa_conf.get(CONF_ENTITY_CONFIG), entity_config=alexa_conf.get(CONF_ENTITY_CONFIG),
) )

View File

@ -39,7 +39,7 @@ async def async_setup(hass):
return True return True
@websocket_api.require_owner @websocket_api.require_admin
@websocket_api.async_response @websocket_api.async_response
async def websocket_list(hass, connection, msg): async def websocket_list(hass, connection, msg):
"""Return a list of users.""" """Return a list of users."""
@ -49,7 +49,7 @@ async def websocket_list(hass, connection, msg):
websocket_api.result_message(msg['id'], result)) websocket_api.result_message(msg['id'], result))
@websocket_api.require_owner @websocket_api.require_admin
@websocket_api.async_response @websocket_api.async_response
async def websocket_delete(hass, connection, msg): async def websocket_delete(hass, connection, msg):
"""Delete a user.""" """Delete a user."""
@ -72,7 +72,7 @@ async def websocket_delete(hass, connection, msg):
websocket_api.result_message(msg['id'])) websocket_api.result_message(msg['id']))
@websocket_api.require_owner @websocket_api.require_admin
@websocket_api.async_response @websocket_api.async_response
async def websocket_create(hass, connection, msg): async def websocket_create(hass, connection, msg):
"""Create a user.""" """Create a user."""

View File

@ -3,7 +3,6 @@ import voluptuous as vol
from homeassistant.auth.providers import homeassistant as auth_ha from homeassistant.auth.providers import homeassistant as auth_ha
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.websocket_api.decorators import require_owner
WS_TYPE_CREATE = 'config/auth_provider/homeassistant/create' WS_TYPE_CREATE = 'config/auth_provider/homeassistant/create'
@ -54,7 +53,7 @@ def _get_provider(hass):
raise RuntimeError('Provider not found') raise RuntimeError('Provider not found')
@require_owner @websocket_api.require_admin
@websocket_api.async_response @websocket_api.async_response
async def websocket_create(hass, connection, msg): async def websocket_create(hass, connection, msg):
"""Create credentials and attach to a user.""" """Create credentials and attach to a user."""
@ -91,7 +90,7 @@ async def websocket_create(hass, connection, msg):
connection.send_message(websocket_api.result_message(msg['id'])) connection.send_message(websocket_api.result_message(msg['id']))
@require_owner @websocket_api.require_admin
@websocket_api.async_response @websocket_api.async_response
async def websocket_delete(hass, connection, msg): async def websocket_delete(hass, connection, msg):
"""Delete username and related credential.""" """Delete username and related credential."""
@ -123,6 +122,7 @@ async def websocket_delete(hass, connection, msg):
websocket_api.result_message(msg['id'])) websocket_api.result_message(msg['id']))
@websocket_api.require_admin
@websocket_api.async_response @websocket_api.async_response
async def websocket_change_password(hass, connection, msg): async def websocket_change_password(hass, connection, msg):
"""Change user password.""" """Change user password."""

View File

@ -1,7 +1,9 @@
"""Http views to control the config manager.""" """Http views to control the config manager."""
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.exceptions import Unauthorized
from homeassistant.helpers.data_entry_flow import ( from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView) FlowManagerIndexView, FlowManagerResourceView)
@ -63,6 +65,9 @@ class ConfigManagerEntryResourceView(HomeAssistantView):
async def delete(self, request, entry_id): async def delete(self, request, entry_id):
"""Delete a config entry.""" """Delete a config entry."""
if not request['hass_user'].is_admin:
raise Unauthorized(config_entry_id=entry_id, permission='remove')
hass = request.app['hass'] hass = request.app['hass']
try: try:
@ -85,12 +90,26 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView):
Example of a non-user initiated flow is a discovered Hue hub that Example of a non-user initiated flow is a discovered Hue hub that
requires user interaction to finish setup. requires user interaction to finish setup.
""" """
if not request['hass_user'].is_admin:
raise Unauthorized(
perm_category=CAT_CONFIG_ENTRIES, permission='add')
hass = request.app['hass'] hass = request.app['hass']
return self.json([ return self.json([
flw for flw in hass.config_entries.flow.async_progress() flw for flw in hass.config_entries.flow.async_progress()
if flw['context']['source'] != config_entries.SOURCE_USER]) if flw['context']['source'] != config_entries.SOURCE_USER])
# pylint: disable=arguments-differ
async def post(self, request):
"""Handle a POST request."""
if not request['hass_user'].is_admin:
raise Unauthorized(
perm_category=CAT_CONFIG_ENTRIES, permission='add')
# pylint: disable=no-value-for-parameter
return await super().post(request)
class ConfigManagerFlowResourceView(FlowManagerResourceView): class ConfigManagerFlowResourceView(FlowManagerResourceView):
"""View to interact with the flow manager.""" """View to interact with the flow manager."""
@ -98,6 +117,24 @@ class ConfigManagerFlowResourceView(FlowManagerResourceView):
url = '/api/config/config_entries/flow/{flow_id}' url = '/api/config/config_entries/flow/{flow_id}'
name = 'api:config:config_entries:flow:resource' name = 'api:config:config_entries:flow:resource'
async def get(self, request, flow_id):
"""Get the current state of a data_entry_flow."""
if not request['hass_user'].is_admin:
raise Unauthorized(
perm_category=CAT_CONFIG_ENTRIES, permission='add')
return await super().get(request, flow_id)
# pylint: disable=arguments-differ
async def post(self, request, flow_id):
"""Handle a POST request."""
if not request['hass_user'].is_admin:
raise Unauthorized(
perm_category=CAT_CONFIG_ENTRIES, permission='add')
# pylint: disable=no-value-for-parameter
return await super().post(request, flow_id)
class ConfigManagerAvailableFlowView(HomeAssistantView): class ConfigManagerAvailableFlowView(HomeAssistantView):
"""View to query available flows.""" """View to query available flows."""

View File

@ -33,7 +33,7 @@ SERVICE_INCREMENT = 'increment'
SERVICE_RESET = 'reset' SERVICE_RESET = 'reset'
SERVICE_SCHEMA = vol.Schema({ SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
}) })
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({

View File

@ -60,7 +60,7 @@ INTENT_OPEN_COVER = 'HassOpenCover'
INTENT_CLOSE_COVER = 'HassCloseCover' INTENT_CLOSE_COVER = 'HassCloseCover'
COVER_SERVICE_SCHEMA = vol.Schema({ COVER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
}) })
COVER_SET_COVER_POSITION_SCHEMA = COVER_SERVICE_SCHEMA.extend({ COVER_SET_COVER_POSITION_SCHEMA = COVER_SERVICE_SCHEMA.extend({

View File

@ -0,0 +1,89 @@
"""Support for ESPHome covers."""
import logging
from typing import TYPE_CHECKING, Optional
from homeassistant.components.cover import CoverDevice, SUPPORT_CLOSE, \
SUPPORT_OPEN, SUPPORT_STOP
from homeassistant.components.esphome import EsphomeEntity, \
platform_async_setup_entry
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_CLOSED, STATE_OPEN
from homeassistant.helpers.typing import HomeAssistantType
if TYPE_CHECKING:
# pylint: disable=unused-import
from aioesphomeapi import CoverInfo, CoverState # noqa
DEPENDENCIES = ['esphome']
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistantType,
entry: ConfigEntry, async_add_entities) -> None:
"""Set up ESPHome covers based on a config entry."""
# pylint: disable=redefined-outer-name
from aioesphomeapi import CoverInfo, CoverState # noqa
await platform_async_setup_entry(
hass, entry, async_add_entities,
component_key='cover',
info_type=CoverInfo, entity_type=EsphomeCover,
state_type=CoverState
)
COVER_STATE_INT_TO_STR = {
0: STATE_OPEN,
1: STATE_CLOSED
}
class EsphomeCover(EsphomeEntity, CoverDevice):
"""A cover implementation for ESPHome."""
@property
def _static_info(self) -> 'CoverInfo':
return super()._static_info
@property
def _state(self) -> Optional['CoverState']:
return super()._state
@property
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
@property
def assumed_state(self) -> bool:
"""Return true if we do optimistic updates."""
return self._static_info.is_optimistic
@property
def is_closed(self) -> Optional[bool]:
"""Return if the cover is closed or not."""
if self._state is None:
return None
return COVER_STATE_INT_TO_STR[self._state.state]
async def async_open_cover(self, **kwargs) -> None:
"""Open the cover."""
from aioesphomeapi.client import COVER_COMMAND_OPEN
await self._client.cover_command(key=self._static_info.key,
command=COVER_COMMAND_OPEN)
async def async_close_cover(self, **kwargs) -> None:
"""Close cover."""
from aioesphomeapi.client import COVER_COMMAND_CLOSE
await self._client.cover_command(key=self._static_info.key,
command=COVER_COMMAND_CLOSE)
async def async_stop_cover(self, **kwargs):
"""Stop the cover."""
from aioesphomeapi.client import COVER_COMMAND_STOP
await self._client.cover_command(key=self._static_info.key,
command=COVER_COMMAND_STOP)

View File

@ -287,7 +287,8 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self):
"""Unsubscribe when removed.""" """Unsubscribe when removed."""
await subscription.async_unsubscribe_topics(self.hass, self._sub_state) self._sub_state = await subscription.async_unsubscribe_topics(
self.hass, self._sub_state)
await MqttAvailability.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self)
@property @property

View File

@ -15,8 +15,8 @@ from homeassistant.components.rflink import (
from homeassistant.components.cover import ( from homeassistant.components.cover import (
CoverDevice, PLATFORM_SCHEMA) CoverDevice, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.const import CONF_NAME, STATE_OPEN
DEPENDENCIES = ['rflink'] DEPENDENCIES = ['rflink']
@ -60,9 +60,17 @@ async def async_setup_platform(hass, config, async_add_entities,
async_add_entities(devices_from_config(config)) async_add_entities(devices_from_config(config))
class RflinkCover(RflinkCommand, CoverDevice): class RflinkCover(RflinkCommand, CoverDevice, RestoreEntity):
"""Rflink entity which can switch on/stop/off (eg: cover).""" """Rflink entity which can switch on/stop/off (eg: cover)."""
async def async_added_to_hass(self):
"""Restore RFLink cover state (OPEN/CLOSE)."""
await super().async_added_to_hass()
old_state = await self.async_get_last_state()
if old_state is not None:
self._state = old_state.state == STATE_OPEN
def _handle_event(self, event): def _handle_event(self, event):
"""Adjust state if Rflink picks up a remote command for this device.""" """Adjust state if Rflink picks up a remote command for this device."""
self.cancel_queued_send_commands() self.cancel_queued_send_commands()

View File

@ -8,20 +8,36 @@ https://home-assistant.io/components/cover.tellduslive/
""" """
import logging import logging
from homeassistant.components import tellduslive from homeassistant.components import cover, tellduslive
from homeassistant.components.cover import CoverDevice from homeassistant.components.cover import CoverDevice
from homeassistant.components.tellduslive.entry import TelldusLiveEntity from homeassistant.components.tellduslive.entry import TelldusLiveEntity
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Telldus Live covers.""" """Old way of setting up TelldusLive.
if discovery_info is None:
return
client = hass.data[tellduslive.DOMAIN] Can only be called when a user accidentally mentions the platform in their
add_entities(TelldusLiveCover(client, cover) for cover in discovery_info) config. But even in that case it would have been ignored.
"""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up tellduslive sensors dynamically."""
async def async_discover_cover(device_id):
"""Discover and add a discovered sensor."""
client = hass.data[tellduslive.DOMAIN]
async_add_entities([TelldusLiveCover(client, device_id)])
async_dispatcher_connect(
hass,
tellduslive.TELLDUS_DISCOVERY_NEW.format(cover.DOMAIN,
tellduslive.DOMAIN),
async_discover_cover,
)
class TelldusLiveCover(TelldusLiveEntity, CoverDevice): class TelldusLiveCover(TelldusLiveEntity, CoverDevice):

View File

@ -18,9 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
model = device['model'] model = device['model']
if model == 'curtain': if model == 'curtain':
devices.append(XiaomiGenericCover(device, "Curtain", devices.append(XiaomiGenericCover(device, "Curtain",
{'status': 'status', 'status', gateway))
'pos': 'curtain_level'},
gateway))
add_entities(devices) add_entities(devices)
@ -45,20 +43,20 @@ class XiaomiGenericCover(XiaomiDevice, CoverDevice):
def close_cover(self, **kwargs): def close_cover(self, **kwargs):
"""Close the cover.""" """Close the cover."""
self._write_to_hub(self._sid, **{self._data_key['status']: 'close'}) self._write_to_hub(self._sid, **{self._data_key: 'close'})
def open_cover(self, **kwargs): def open_cover(self, **kwargs):
"""Open the cover.""" """Open the cover."""
self._write_to_hub(self._sid, **{self._data_key['status']: 'open'}) self._write_to_hub(self._sid, **{self._data_key: 'open'})
def stop_cover(self, **kwargs): def stop_cover(self, **kwargs):
"""Stop the cover.""" """Stop the cover."""
self._write_to_hub(self._sid, **{self._data_key['status']: 'stop'}) self._write_to_hub(self._sid, **{self._data_key: 'stop'})
def set_cover_position(self, **kwargs): def set_cover_position(self, **kwargs):
"""Move the cover to a specific position.""" """Move the cover to a specific position."""
position = kwargs.get(ATTR_POSITION) position = kwargs.get(ATTR_POSITION)
self._write_to_hub(self._sid, **{self._data_key['pos']: str(position)}) self._write_to_hub(self._sid, **{ATTR_CURTAIN_LEVEL: str(position)})
def parse_data(self, data, raw_data): def parse_data(self, data, raw_data):
"""Parse data sent by gateway.""" """Parse data sent by gateway."""

View File

@ -1,139 +0,0 @@
"""
Platform for the Daikin AC.
For more details about this component, please refer to the documentation
https://home-assistant.io/components/daikin/
"""
import logging
from datetime import timedelta
from socket import timeout
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.discovery import SERVICE_DAIKIN
from homeassistant.const import (
CONF_HOSTS, CONF_ICON, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_TYPE
)
from homeassistant.helpers import discovery
from homeassistant.helpers.discovery import load_platform
from homeassistant.util import Throttle
REQUIREMENTS = ['pydaikin==0.8']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'daikin'
ATTR_TARGET_TEMPERATURE = 'target_temperature'
ATTR_INSIDE_TEMPERATURE = 'inside_temperature'
ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
COMPONENT_TYPES = ['climate', 'sensor']
SENSOR_TYPE_TEMPERATURE = 'temperature'
SENSOR_TYPES = {
ATTR_INSIDE_TEMPERATURE: {
CONF_NAME: 'Inside Temperature',
CONF_ICON: 'mdi:thermometer',
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
},
ATTR_OUTSIDE_TEMPERATURE: {
CONF_NAME: 'Outside Temperature',
CONF_ICON: 'mdi:thermometer',
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
}
}
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(
CONF_HOSTS, default=[]
): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(
CONF_MONITORED_CONDITIONS,
default=list(SENSOR_TYPES.keys())
): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)])
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Establish connection with Daikin."""
def discovery_dispatch(service, discovery_info):
"""Dispatcher for Daikin discovery events."""
host = discovery_info.get('ip')
if daikin_api_setup(hass, host) is None:
return
for component in COMPONENT_TYPES:
load_platform(hass, component, DOMAIN, discovery_info,
config)
discovery.listen(hass, SERVICE_DAIKIN, discovery_dispatch)
for host in config.get(DOMAIN, {}).get(CONF_HOSTS, []):
if daikin_api_setup(hass, host) is None:
continue
discovery_info = {
'ip': host,
CONF_MONITORED_CONDITIONS:
config[DOMAIN][CONF_MONITORED_CONDITIONS]
}
load_platform(hass, 'sensor', DOMAIN, discovery_info, config)
return True
def daikin_api_setup(hass, host, name=None):
"""Create a Daikin instance only once."""
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
api = hass.data[DOMAIN].get(host)
if api is None:
from pydaikin import appliance
try:
device = appliance.Appliance(host)
except timeout:
_LOGGER.error("Connection to Daikin could not be established")
return False
if name is None:
name = device.values['name']
api = DaikinApi(device, name)
return api
class DaikinApi:
"""Keep the Daikin instance in one place and centralize the update."""
def __init__(self, device, name):
"""Initialize the Daikin Handle."""
self.device = device
self.name = name
self.ip_address = device.ip
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self, **kwargs):
"""Pull the latest data from Daikin."""
try:
self.device.update_status()
except timeout:
_LOGGER.warning(
"Connection failed for %s", self.ip_address
)
@property
def mac(self):
"""Return mac-address of device."""
return self.device.values.get('mac')

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "El dispositiu ja est\u00e0 configurat",
"device_fail": "S'ha produ\u00eft un error inesperat al crear el dispositiu.",
"device_timeout": "S'ha acabat el temps d'espera en la connexi\u00f3 amb el dispositiu."
},
"step": {
"user": {
"data": {
"host": "Amfitri\u00f3"
},
"description": "Introdueix l'adre\u00e7a IP del teu Daikin AC.",
"title": "Configuraci\u00f3 de Daikin AC"
}
},
"title": "Daikin AC"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured",
"device_fail": "Unexpected error creating device.",
"device_timeout": "Timeout connecting to the device."
},
"step": {
"user": {
"data": {
"host": "Host"
},
"description": "Enter IP address of your Daikin AC.",
"title": "Configure Daikin AC"
}
},
"title": "Daikin AC"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "\uc7a5\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"device_fail": "\uc7a5\uce58\ub97c \uad6c\uc131\ud558\ub294\uc911 \uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
"device_timeout": "\uc7a5\uce58 \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4."
},
"step": {
"user": {
"data": {
"host": "\ud638\uc2a4\ud2b8"
},
"description": "Daikin AC \uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
"title": "Daikin AC \uad6c\uc131"
}
},
"title": "Daikin AC"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "Apparat ass scho konfigur\u00e9iert",
"device_fail": "Onerwaarte Feeler beim erstelle vum Apparat.",
"device_timeout": "Z\u00e4it Iwwerschreidung beim verbannen mam Apparat."
},
"step": {
"user": {
"data": {
"host": "Apparat"
},
"description": "Gitt d'IP Adresse vum Daikin AC an:",
"title": "Daikin AC konfigur\u00e9ieren"
}
},
"title": "Daikin AC"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
"device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.",
"device_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443."
},
"step": {
"user": {
"data": {
"host": "\u0425\u043e\u0441\u0442"
},
"description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0432\u0430\u0448\u0435\u0433\u043e Daikin AC.",
"title": "Daikin AC"
}
},
"title": "Daikin AC"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "Naprava je \u017ee konfigurirana",
"device_fail": "Nepri\u010dakovana napaka pri ustvarjanju naprave.",
"device_timeout": "\u010casovna omejitev za priklop na napravo je potekla."
},
"step": {
"user": {
"data": {
"host": "Gostitelj"
},
"description": "Vnesite naslov IP va\u0161e Daikin klime.",
"title": "Nastavite Daikin klimo"
}
},
"title": "Daikin AC"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"device_fail": "\u5275\u5efa\u88dd\u7f6e\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002",
"device_timeout": "\u9023\u7dda\u81f3\u88dd\u7f6e\u903e\u6642\u3002"
},
"step": {
"user": {
"data": {
"host": "\u4e3b\u6a5f\u7aef"
},
"description": "\u8f38\u5165\u60a8\u7684\u5927\u91d1\u7a7a\u8abf IP \u4f4d\u5740\u3002",
"title": "\u8a2d\u5b9a\u5927\u91d1\u7a7a\u8abf"
}
},
"title": "\u5927\u91d1\u7a7a\u8abf\uff08Daikin AC\uff09"
}
}

View File

@ -0,0 +1,146 @@
"""
Platform for the Daikin AC.
For more details about this component, please refer to the documentation
https://home-assistant.io/components/daikin/
"""
import asyncio
from datetime import timedelta
import logging
from socket import timeout
import async_timeout
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_HOSTS
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import Throttle
from . import config_flow # noqa pylint_disable=unused-import
from .const import KEY_HOST
REQUIREMENTS = ['pydaikin==0.9']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'daikin'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
COMPONENT_TYPES = ['climate', 'sensor']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(
CONF_HOSTS, default=[]
): vol.All(cv.ensure_list, [cv.string]),
})
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass, config):
"""Establish connection with Daikin."""
if DOMAIN not in config:
return True
hosts = config[DOMAIN].get(CONF_HOSTS)
if not hosts:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={'source': SOURCE_IMPORT}))
for host in hosts:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={'source': SOURCE_IMPORT},
data={
KEY_HOST: host,
}))
return True
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Establish connection with Daikin."""
conf = entry.data
daikin_api = await daikin_api_setup(hass, conf[KEY_HOST])
if not daikin_api:
return False
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api})
await asyncio.wait([
hass.config_entries.async_forward_entry_setup(entry, component)
for component in COMPONENT_TYPES
])
return True
async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
await asyncio.wait([
hass.config_entries.async_forward_entry_unload(config_entry, component)
for component in COMPONENT_TYPES
])
hass.data[DOMAIN].pop(config_entry.entry_id)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
return True
async def daikin_api_setup(hass, host):
"""Create a Daikin instance only once."""
from pydaikin.appliance import Appliance
try:
with async_timeout.timeout(10):
device = await hass.async_add_executor_job(Appliance, host)
except asyncio.TimeoutError:
_LOGGER.error("Connection to Daikin could not be established")
return None
except Exception: # pylint: disable=broad-except
_LOGGER.error("Unexpected error creating device")
return None
name = device.values['name']
api = DaikinApi(device, name)
return api
class DaikinApi:
"""Keep the Daikin instance in one place and centralize the update."""
def __init__(self, device, name):
"""Initialize the Daikin Handle."""
self.device = device
self.name = name
self.ip_address = device.ip
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self, **kwargs):
"""Pull the latest data from Daikin."""
try:
self.device.update_status()
except timeout:
_LOGGER.warning(
"Connection failed for %s", self.ip_address
)
@property
def mac(self):
"""Return mac-address of device."""
return self.device.values.get(CONNECTION_NETWORK_MAC)
@property
def device_info(self):
"""Return a device description for device registry."""
info = self.device.values
return {
'connections': {(CONNECTION_NETWORK_MAC, self.mac)},
'identifieres': self.mac,
'manufacturer': 'Daikin',
'model': info.get('model'),
'name': info.get('name'),
'sw_version': info.get('ver').replace('_', '.'),
}

View File

@ -0,0 +1,74 @@
"""Config flow for the Daikin platform."""
import asyncio
import logging
import async_timeout
import voluptuous as vol
from homeassistant import config_entries
from .const import KEY_HOST, KEY_IP, KEY_MAC
_LOGGER = logging.getLogger(__name__)
@config_entries.HANDLERS.register('daikin')
class FlowHandler(config_entries.ConfigFlow):
"""Handle a config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
async def _create_entry(self, host, mac):
"""Register new entry."""
# Check if mac already is registered
for entry in self._async_current_entries():
if entry.data[KEY_MAC] == mac:
return self.async_abort(reason='already_configured')
return self.async_create_entry(
title=host,
data={
KEY_HOST: host,
KEY_MAC: mac
})
async def _create_device(self, host):
"""Create device."""
from pydaikin.appliance import Appliance
try:
with async_timeout.timeout(10):
device = await self.hass.async_add_executor_job(
Appliance, host)
except asyncio.TimeoutError:
return self.async_abort(reason='device_timeout')
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected error creating device")
return self.async_abort(reason='device_fail')
mac = device.values.get('mac')
return await self._create_entry(host, mac)
async def async_step_user(self, user_input=None):
"""User initiated config flow."""
if user_input is None:
return self.async_show_form(
step_id='user',
data_schema=vol.Schema({
vol.Required(KEY_HOST): str
})
)
return await self._create_device(user_input[KEY_HOST])
async def async_step_import(self, user_input):
"""Import a config entry."""
host = user_input.get(KEY_HOST)
if not host:
return await self.async_step_user()
return await self._create_device(host)
async def async_step_discovery(self, user_input):
"""Initialize step from discovery."""
_LOGGER.info("Discovered device: %s", user_input)
return await self._create_entry(user_input[KEY_IP],
user_input[KEY_MAC])

View File

@ -0,0 +1,25 @@
"""Constants for Daikin."""
from homeassistant.const import CONF_ICON, CONF_NAME, CONF_TYPE
ATTR_TARGET_TEMPERATURE = 'target_temperature'
ATTR_INSIDE_TEMPERATURE = 'inside_temperature'
ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature'
SENSOR_TYPE_TEMPERATURE = 'temperature'
SENSOR_TYPES = {
ATTR_INSIDE_TEMPERATURE: {
CONF_NAME: 'Inside Temperature',
CONF_ICON: 'mdi:thermometer',
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
},
ATTR_OUTSIDE_TEMPERATURE: {
CONF_NAME: 'Outside Temperature',
CONF_ICON: 'mdi:thermometer',
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
}
}
KEY_HOST = 'host'
KEY_MAC = 'mac'
KEY_IP = 'ip'

View File

@ -0,0 +1,19 @@
{
"config": {
"title": "Daikin AC",
"step": {
"user": {
"title": "Configure Daikin AC",
"description": "Enter IP address of your Daikin AC.",
"data": {
"host": "Host"
}
}
},
"abort": {
"device_timeout": "Timeout connecting to the device.",
"device_fail": "Unexpected error creating device.",
"already_configured": "Device is already configured"
}
}
}

View File

@ -14,7 +14,7 @@
"host": "Amfitri\u00f3", "host": "Amfitri\u00f3",
"port": "Port" "port": "Port"
}, },
"title": "Definiu la passarel\u00b7la deCONZ" "title": "Definici\u00f3 de la passarel\u00b7la deCONZ"
}, },
"link": { "link": {
"description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ\n2. Prem el bot\u00f3 \"Desbloquejar passarel\u00b7la\"", "description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ\n2. Prem el bot\u00f3 \"Desbloquejar passarel\u00b7la\"",
@ -23,7 +23,7 @@
"options": { "options": {
"data": { "data": {
"allow_clip_sensor": "Permet la importaci\u00f3 de sensors virtuals", "allow_clip_sensor": "Permet la importaci\u00f3 de sensors virtuals",
"allow_deconz_groups": "Permet la importaci\u00f3 de grups deCONZ" "allow_deconz_groups": "Permetre la importaci\u00f3 de grups deCONZ"
}, },
"title": "Opcions de configuraci\u00f3 addicionals per deCONZ" "title": "Opcions de configuraci\u00f3 addicionals per deCONZ"
} }

View File

@ -22,8 +22,10 @@
}, },
"options": { "options": {
"data": { "data": {
"allow_clip_sensor": "Virtu\u00e1lis szenzorok import\u00e1l\u00e1s\u00e1nak enged\u00e9lyez\u00e9se" "allow_clip_sensor": "Virtu\u00e1lis szenzorok import\u00e1l\u00e1s\u00e1nak enged\u00e9lyez\u00e9se",
} "allow_deconz_groups": "deCONZ csoportok import\u00e1l\u00e1s\u00e1nak enged\u00e9lyez\u00e9se"
},
"title": "Extra be\u00e1ll\u00edt\u00e1si lehet\u0151s\u00e9gek a deCONZhoz"
} }
}, },
"title": "deCONZ Zigbee gateway" "title": "deCONZ Zigbee gateway"

View File

@ -28,6 +28,6 @@
"title": "Extra Konfiguratiouns Optiounen fir deCONZ" "title": "Extra Konfiguratiouns Optiounen fir deCONZ"
} }
}, },
"title": "deCONZ" "title": "deCONZ Zigbee gateway"
} }
} }

View File

@ -15,6 +15,7 @@ DEPENDENCIES = ['conversation', 'introduction', 'zone']
DOMAIN = 'demo' DOMAIN = 'demo'
COMPONENTS_WITH_DEMO_PLATFORM = [ COMPONENTS_WITH_DEMO_PLATFORM = [
'air_quality',
'alarm_control_panel', 'alarm_control_panel',
'binary_sensor', 'binary_sensor',
'calendar', 'calendar',

View File

@ -24,9 +24,15 @@ BT_PREFIX = 'BT_'
CONF_REQUEST_RSSI = 'request_rssi' CONF_REQUEST_RSSI = 'request_rssi'
CONF_DEVICE_ID = "device_id"
DEFAULT_DEVICE_ID = -1
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TRACK_NEW): cv.boolean, vol.Optional(CONF_TRACK_NEW): cv.boolean,
vol.Optional(CONF_REQUEST_RSSI): cv.boolean vol.Optional(CONF_REQUEST_RSSI): cv.boolean,
vol.Optional(CONF_DEVICE_ID, default=DEFAULT_DEVICE_ID):
vol.All(vol.Coerce(int), vol.Range(min=-1))
}) })
@ -44,11 +50,13 @@ def setup_scanner(hass, config, see, discovery_info=None):
see(mac="{}{}".format(BT_PREFIX, mac), host_name=name, see(mac="{}{}".format(BT_PREFIX, mac), host_name=name,
attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH) attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH)
device_id = config.get(CONF_DEVICE_ID)
def discover_devices(): def discover_devices():
"""Discover Bluetooth devices.""" """Discover Bluetooth devices."""
result = bluetooth.discover_devices( result = bluetooth.discover_devices(
duration=8, lookup_names=True, flush_cache=True, duration=8, lookup_names=True, flush_cache=True,
lookup_class=False) lookup_class=False, device_id=device_id)
_LOGGER.debug("Bluetooth devices discovered = %d", len(result)) _LOGGER.debug("Bluetooth devices discovered = %d", len(result))
return result return result

View File

@ -1,56 +1,25 @@
""" """
Support for device tracking through Freebox routers. Support for Freebox devices (Freebox v6 and Freebox mini 4K).
This tracker keeps track of the devices connected to the configured Freebox. For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.freebox/ https://home-assistant.io/components/device_tracker.freebox/
""" """
import asyncio
import copy
import logging
import socket
from collections import namedtuple from collections import namedtuple
from datetime import timedelta import logging
import voluptuous as vol from homeassistant.components.device_tracker import DeviceScanner
from homeassistant.components.freebox import DATA_FREEBOX
import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['freebox']
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
from homeassistant.const import (
CONF_HOST, CONF_PORT)
REQUIREMENTS = ['aiofreepybox==0.0.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
FREEBOX_CONFIG_FILE = 'freebox.conf'
PLATFORM_SCHEMA = vol.All(
PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.port
}))
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up the Freebox device tracker and start the polling."""
freebox_config = copy.deepcopy(config)
if discovery_info is not None:
freebox_config[CONF_HOST] = discovery_info['properties']['api_domain']
freebox_config[CONF_PORT] = discovery_info['properties']['https_port']
_LOGGER.info("Discovered Freebox server: %s:%s",
freebox_config[CONF_HOST], freebox_config[CONF_PORT])
scanner = FreeboxDeviceScanner(hass, freebox_config, async_see)
interval = freebox_config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
await scanner.async_start(hass, interval)
return True
async def async_get_scanner(hass, config):
"""Validate the configuration and return a Freebox scanner."""
scanner = FreeboxDeviceScanner(hass.data[DATA_FREEBOX])
await scanner.async_connect()
return scanner if scanner.success_init else None
Device = namedtuple('Device', ['id', 'name', 'ip']) Device = namedtuple('Device', ['id', 'name', 'ip'])
@ -62,59 +31,41 @@ def _build_device(device_dict):
device_dict['l3connectivities'][0]['addr']) device_dict['l3connectivities'][0]['addr'])
class FreeboxDeviceScanner: class FreeboxDeviceScanner(DeviceScanner):
"""This class scans for devices connected to the Freebox.""" """Queries the Freebox device."""
def __init__(self, hass, config, async_see): def __init__(self, fbx):
"""Initialize the scanner.""" """Initialize the scanner."""
from aiofreepybox import Freepybox self.last_results = {}
self.success_init = False
self.connection = fbx
self.host = config[CONF_HOST] async def async_connect(self):
self.port = config[CONF_PORT] """Initialize connection to the router."""
self.token_file = hass.config.path(FREEBOX_CONFIG_FILE) # Test the router is accessible.
self.async_see = async_see data = await self.connection.lan.get_hosts_list()
self.success_init = data is not None
# Hardcode the app description to avoid invalidating the authentication async def async_scan_devices(self):
# file at each new version. """Scan for new devices and return a list with found device IDs."""
# The version can be changed if we want the user to re-authorize HASS
# on her Freebox.
app_desc = {
'app_id': 'hass',
'app_name': 'Home Assistant',
'app_version': '0.65',
'device_name': socket.gethostname()
}
api_version = 'v1' # Use the lowest working version.
self.fbx = Freepybox(
app_desc=app_desc,
token_file=self.token_file,
api_version=api_version)
async def async_start(self, hass, interval):
"""Perform a first update and start polling at the given interval."""
await self.async_update_info() await self.async_update_info()
interval = max(interval, MIN_TIME_BETWEEN_SCANS) return [device.id for device in self.last_results]
async_track_time_interval(hass, self.async_update_info, interval)
async def async_update_info(self, now=None): async def get_device_name(self, device):
"""Check the Freebox for devices.""" """Return the name of the given device or None if we don't know."""
from aiofreepybox.exceptions import HttpRequestError name = next((
result.name for result in self.last_results
if result.id == device), None)
return name
_LOGGER.info('Scanning devices') async def async_update_info(self):
"""Ensure the information from the Freebox router is up to date."""
_LOGGER.debug('Checking Devices')
await self.fbx.open(self.host, self.port) hosts = await self.connection.lan.get_hosts_list()
try:
hosts = await self.fbx.lan.get_hosts_list()
except HttpRequestError:
_LOGGER.exception('Failed to scan devices')
else:
active_devices = [_build_device(device)
for device in hosts
if device['active']]
if active_devices: last_results = [_build_device(device)
await asyncio.wait([self.async_see(mac=d.id, host_name=d.name) for device in hosts
for d in active_devices]) if device['active']]
await self.fbx.close() self.last_results = last_results

View File

@ -23,10 +23,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_URL): cv.url, vol.Optional(CONF_URL): cv.url,
}) })
HOSTS_PATH = "wlan_host_list.Hosts"
def get_scanner(hass, config): def get_scanner(hass, config):
"""Get a Huawei LTE router scanner.""" """Get a Huawei LTE router scanner."""
data = hass.data[DATA_KEY].get_data(config) data = hass.data[DATA_KEY].get_data(config)
data.subscribe(HOSTS_PATH)
return HuaweiLteScanner(data) return HuaweiLteScanner(data)
@ -43,7 +46,7 @@ class HuaweiLteScanner(DeviceScanner):
self.data.update() self.data.update()
self._hosts = { self._hosts = {
x["MacAddress"]: x x["MacAddress"]: x
for x in self.data["wlan_host_list.Hosts.Host"] for x in self.data[HOSTS_PATH + ".Host"]
if x.get("MacAddress") if x.get("MacAddress")
} }
return list(self._hosts) return list(self._hosts)

View File

@ -200,7 +200,9 @@ class Icloud(DeviceScanner):
self._intervals = {} self._intervals = {}
for device in self.api.devices: for device in self.api.devices:
status = device.status(DEVICESTATUSSET) status = device.status(DEVICESTATUSSET)
_LOGGER.debug('Device Status is %s', status)
devicename = slugify(status['name'].replace(' ', '', 99)) devicename = slugify(status['name'].replace(' ', '', 99))
_LOGGER.info('Adding icloud device: %s', devicename)
if devicename in self.devices: if devicename in self.devices:
_LOGGER.error('Multiple devices with name: %s', devicename) _LOGGER.error('Multiple devices with name: %s', devicename)
continue continue
@ -404,6 +406,7 @@ class Icloud(DeviceScanner):
continue continue
status = device.status(DEVICESTATUSSET) status = device.status(DEVICESTATUSSET)
_LOGGER.debug('Device Status is %s', status)
dev_id = status['name'].replace(' ', '', 99) dev_id = status['name'].replace(' ', '', 99)
dev_id = slugify(dev_id) dev_id = slugify(dev_id)
attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get( attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get(
@ -441,9 +444,9 @@ class Icloud(DeviceScanner):
return return
self.api.authenticate() self.api.authenticate()
for device in self.api.devices: for device in self.api.devices:
if devicename is None or device == self.devices[devicename]: if str(device) == str(self.devices[devicename]):
_LOGGER.info("Playing Lost iPhone sound for %s", devicename)
device.play_sound() device.play_sound()
def update_icloud(self, devicename=None): def update_icloud(self, devicename=None):

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PORT, CONF_PASSWORD, CONF_USERNAME CONF_HOST, CONF_PORT, CONF_PASSWORD, CONF_USERNAME
) )
REQUIREMENTS = ['ndms2_client==0.0.5'] REQUIREMENTS = ['ndms2_client==0.0.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL,
CONF_DEVICES, CONF_EXCLUDE) CONF_DEVICES, CONF_EXCLUDE)
REQUIREMENTS = ['pynetgear==0.5.1'] REQUIREMENTS = ['pynetgear==0.5.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -12,20 +12,22 @@ import voluptuous as vol
from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL,
CONF_PASSWORD, CONF_USERNAME) CONF_PASSWORD, CONF_USERNAME, ATTR_BATTERY_LEVEL)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util import slugify from homeassistant.util import slugify
REQUIREMENTS = ['pytraccar==0.1.2'] REQUIREMENTS = ['pytraccar==0.2.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_ADDRESS = 'address' ATTR_ADDRESS = 'address'
ATTR_CATEGORY = 'category' ATTR_CATEGORY = 'category'
ATTR_GEOFENCE = 'geofence' ATTR_GEOFENCE = 'geofence'
ATTR_MOTION = 'motion'
ATTR_SPEED = 'speed'
ATTR_TRACKER = 'tracker' ATTR_TRACKER = 'tracker'
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
@ -78,13 +80,21 @@ class TraccarScanner:
await self._api.get_device_info() await self._api.get_device_info()
for devicename in self._api.device_info: for devicename in self._api.device_info:
device = self._api.device_info[devicename] device = self._api.device_info[devicename]
device_attributes = { attr = {}
ATTR_ADDRESS: device['address'], attr[ATTR_TRACKER] = 'traccar'
ATTR_GEOFENCE: device['geofence'], if device.get('address') is not None:
ATTR_CATEGORY: device['category'], attr[ATTR_ADDRESS] = device['address']
ATTR_TRACKER: 'traccar' if device.get('geofence') is not None:
} attr[ATTR_GEOFENCE] = device['geofence']
if device.get('category') is not None:
attr[ATTR_CATEGORY] = device['category']
if device.get('speed') is not None:
attr[ATTR_SPEED] = device['speed']
if device.get('battery') is not None:
attr[ATTR_BATTERY_LEVEL] = device['battery']
if device.get('motion') is not None:
attr[ATTR_MOTION] = device['motion']
await self._async_see( await self._async_see(
dev_id=slugify(device['device_id']), dev_id=slugify(device['device_id']),
gps=(device['latitude'], device['longitude']), gps=(device.get('latitude'), device.get('longitude')),
attributes=device_attributes) attributes=attr)

View File

@ -1,16 +1,16 @@
{ {
"config": { "config": {
"abort": { "abort": {
"not_internet_accessible": "La vostra inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Dialogflow.", "not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Dialogflow.",
"one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
}, },
"create_entry": { "create_entry": {
"default": "Per enviar esdeveniments a Home Assistant, haureu de configurar [integraci\u00f3 webhook de Dialogflow]({dialogflow_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - M\u00e8tode: POST \n - Tipus de contingut: application/json\n\nVegeu [la documentaci\u00f3]({docs_url}) per a m\u00e9s detalls." "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar la [integraci\u00f3 webhook de Dialogflow]({dialogflow_url}). \n\n Completa la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/json\n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls."
}, },
"step": { "step": {
"user": { "user": {
"description": "Esteu segur que voleu configurar Dialogflow?", "description": "Est\u00e0s segur que vols configurar Dialogflow?",
"title": "Configureu el Webhook de Dialogflow" "title": "Configuraci\u00f3 del Webhook de Dialogflow"
} }
}, },
"title": "Dialogflow" "title": "Dialogflow"

View File

@ -1,7 +1,12 @@
{ {
"config": { "config": {
"abort": {
"not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a Dialogflow \u00fczenetek fogad\u00e1s\u00e1hoz.",
"one_instance_allowed": "Csak egyetlen konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges."
},
"step": { "step": {
"user": { "user": {
"description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az Dialogflowt?",
"title": "Dialogflow Webhook be\u00e1ll\u00edt\u00e1sa" "title": "Dialogflow Webhook be\u00e1ll\u00edt\u00e1sa"
} }
}, },

View File

@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==2.2.0'] REQUIREMENTS = ['netdisco==2.3.0']
DOMAIN = 'discovery' DOMAIN = 'discovery'
@ -44,13 +44,20 @@ SERVICE_SABNZBD = 'sabnzbd'
SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
SERVICE_HOMEKIT = 'homekit' SERVICE_HOMEKIT = 'homekit'
SERVICE_OCTOPRINT = 'octoprint' SERVICE_OCTOPRINT = 'octoprint'
SERVICE_FREEBOX = 'freebox'
SERVICE_IGD = 'igd'
SERVICE_DLNA_DMR = 'dlna_dmr'
CONFIG_ENTRY_HANDLERS = { CONFIG_ENTRY_HANDLERS = {
SERVICE_DAIKIN: 'daikin',
SERVICE_DECONZ: 'deconz', SERVICE_DECONZ: 'deconz',
'esphome': 'esphome',
'google_cast': 'cast', 'google_cast': 'cast',
SERVICE_HUE: 'hue', SERVICE_HUE: 'hue',
SERVICE_TELLDUSLIVE: 'tellduslive',
SERVICE_IKEA_TRADFRI: 'tradfri', SERVICE_IKEA_TRADFRI: 'tradfri',
'sonos': 'sonos', 'sonos': 'sonos',
SERVICE_IGD: 'upnp',
} }
SERVICE_HANDLERS = { SERVICE_HANDLERS = {
@ -62,12 +69,11 @@ SERVICE_HANDLERS = {
SERVICE_APPLE_TV: ('apple_tv', None), SERVICE_APPLE_TV: ('apple_tv', None),
SERVICE_WINK: ('wink', None), SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None),
SERVICE_DAIKIN: ('daikin', None),
SERVICE_SABNZBD: ('sabnzbd', None), SERVICE_SABNZBD: ('sabnzbd', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
SERVICE_KONNECTED: ('konnected', None), SERVICE_KONNECTED: ('konnected', None),
SERVICE_OCTOPRINT: ('octoprint', None), SERVICE_OCTOPRINT: ('octoprint', None),
SERVICE_FREEBOX: ('freebox', None),
'panasonic_viera': ('media_player', 'panasonic_viera'), 'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'), 'plex_mediaserver': ('media_player', 'plex'),
'roku': ('media_player', 'roku'), 'roku': ('media_player', 'roku'),
@ -87,12 +93,11 @@ SERVICE_HANDLERS = {
'volumio': ('media_player', 'volumio'), 'volumio': ('media_player', 'volumio'),
'lg_smart_device': ('media_player', 'lg_soundbar'), 'lg_smart_device': ('media_player', 'lg_soundbar'),
'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'),
'freebox': ('device_tracker', 'freebox'),
} }
OPTIONAL_SERVICE_HANDLERS = { OPTIONAL_SERVICE_HANDLERS = {
SERVICE_HOMEKIT: ('homekit_controller', None), SERVICE_HOMEKIT: ('homekit_controller', None),
'dlna_dmr': ('media_player', 'dlna_dmr'), SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'),
} }
CONF_IGNORE = 'ignore' CONF_IGNORE = 'ignore'
@ -134,7 +139,7 @@ async def async_setup(hass, config):
discovery_hash = json.dumps([service, info], sort_keys=True) discovery_hash = json.dumps([service, info], sort_keys=True)
if discovery_hash in already_discovered: if discovery_hash in already_discovered:
logger.debug("Already discoverd service %s %s.", service, info) logger.debug("Already discovered service %s %s.", service, info)
return return
already_discovered.add(discovery_hash) already_discovered.add(discovery_hash)
@ -169,20 +174,23 @@ async def async_setup(hass, config):
async def scan_devices(now): async def scan_devices(now):
"""Scan for devices.""" """Scan for devices."""
results = await hass.async_add_job(_discover, netdisco) try:
results = await hass.async_add_job(_discover, netdisco)
for result in results: for result in results:
hass.async_create_task(new_service_found(*result)) hass.async_create_task(new_service_found(*result))
except OSError:
logger.error("Network is unreachable")
async_track_point_in_utc_time(hass, scan_devices, async_track_point_in_utc_time(
dt_util.utcnow() + SCAN_INTERVAL) hass, scan_devices, dt_util.utcnow() + SCAN_INTERVAL)
@callback @callback
def schedule_first(event): def schedule_first(event):
"""Schedule the first discovery when Home Assistant starts up.""" """Schedule the first discovery when Home Assistant starts up."""
async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow()) async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow())
# discovery local services # Discovery for local services
if 'HASSIO' in os.environ: if 'HASSIO' in os.environ:
hass.async_create_task(new_service_found(SERVICE_HASSIO, {})) hass.async_create_task(new_service_found(SERVICE_HASSIO, {}))

View File

@ -104,7 +104,7 @@ def setup(hass, config):
return False return False
# Subscribe to doorbell or motion events # Subscribe to doorbell or motion events
if events is not None: if events:
doorstation.update_schedule(hass) doorstation.update_schedule(hass)
hass.data[DOMAIN] = doorstations hass.data[DOMAIN] = doorstations

View File

@ -26,7 +26,7 @@ EDP_REDY = 'edp_redy'
DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN)
UPDATE_INTERVAL = 60 UPDATE_INTERVAL = 60
REQUIREMENTS = ['edp_redy==0.0.2'] REQUIREMENTS = ['edp_redy==0.0.3']
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({

View File

@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
REQUIREMENTS = ['pyeight==0.0.9'] REQUIREMENTS = ['pyeight==0.1.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -11,12 +11,12 @@ import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_TIMEOUT
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
REQUIREMENTS = ['pyenvisalink==3.7'] REQUIREMENTS = ['pyenvisalink==3.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -46,6 +46,7 @@ DEFAULT_KEEPALIVE = 60
DEFAULT_ZONEDUMP_INTERVAL = 30 DEFAULT_ZONEDUMP_INTERVAL = 30
DEFAULT_ZONETYPE = 'opening' DEFAULT_ZONETYPE = 'opening'
DEFAULT_PANIC = 'Police' DEFAULT_PANIC = 'Police'
DEFAULT_TIMEOUT = 10
SIGNAL_ZONE_UPDATE = 'envisalink.zones_updated' SIGNAL_ZONE_UPDATE = 'envisalink.zones_updated'
SIGNAL_PARTITION_UPDATE = 'envisalink.partition_updated' SIGNAL_PARTITION_UPDATE = 'envisalink.partition_updated'
@ -65,7 +66,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.All(cv.string, vol.In(['HONEYWELL', 'DSC'])), vol.All(cv.string, vol.In(['HONEYWELL', 'DSC'])),
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASS): cv.string, vol.Required(CONF_PASS): cv.string,
vol.Required(CONF_CODE): cv.string, vol.Optional(CONF_CODE): cv.string,
vol.Optional(CONF_PANIC, default=DEFAULT_PANIC): cv.string, vol.Optional(CONF_PANIC, default=DEFAULT_PANIC): cv.string,
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA}, vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA},
@ -77,9 +78,21 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional( vol.Optional(
CONF_ZONEDUMP_INTERVAL, CONF_ZONEDUMP_INTERVAL,
default=DEFAULT_ZONEDUMP_INTERVAL): vol.Coerce(int), default=DEFAULT_ZONEDUMP_INTERVAL): vol.Coerce(int),
vol.Optional(
CONF_TIMEOUT,
default=DEFAULT_TIMEOUT): vol.Coerce(int),
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
SERVICE_CUSTOM_FUNCTION = 'invoke_custom_function'
ATTR_CUSTOM_FUNCTION = 'pgm'
ATTR_PARTITION = 'partition'
SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_CUSTOM_FUNCTION): cv.string,
vol.Required(ATTR_PARTITION): cv.string,
})
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up for Envisalink devices.""" """Set up for Envisalink devices."""
@ -99,11 +112,12 @@ async def async_setup(hass, config):
zone_dump = conf.get(CONF_ZONEDUMP_INTERVAL) zone_dump = conf.get(CONF_ZONEDUMP_INTERVAL)
zones = conf.get(CONF_ZONES) zones = conf.get(CONF_ZONES)
partitions = conf.get(CONF_PARTITIONS) partitions = conf.get(CONF_PARTITIONS)
connection_timeout = conf.get(CONF_TIMEOUT)
sync_connect = asyncio.Future(loop=hass.loop) sync_connect = asyncio.Future(loop=hass.loop)
controller = EnvisalinkAlarmPanel( controller = EnvisalinkAlarmPanel(
host, port, panel_type, version, user, password, zone_dump, host, port, panel_type, version, user, password, zone_dump,
keep_alive, hass.loop) keep_alive, hass.loop, connection_timeout)
hass.data[DATA_EVL] = controller hass.data[DATA_EVL] = controller
@callback @callback
@ -153,6 +167,12 @@ async def async_setup(hass, config):
_LOGGER.info("Shutting down Envisalink") _LOGGER.info("Shutting down Envisalink")
controller.stop() controller.stop()
async def handle_custom_function(call):
"""Handle custom/PGM service."""
custom_function = call.data.get(ATTR_CUSTOM_FUNCTION)
partition = call.data.get(ATTR_PARTITION)
controller.command_output(code, partition, custom_function)
controller.callback_zone_timer_dump = zones_updated_callback controller.callback_zone_timer_dump = zones_updated_callback
controller.callback_zone_state_change = zones_updated_callback controller.callback_zone_state_change = zones_updated_callback
controller.callback_partition_state_change = partition_updated_callback controller.callback_partition_state_change = partition_updated_callback
@ -190,6 +210,11 @@ async def async_setup(hass, config):
}, config }, config
)) ))
hass.services.async_register(DOMAIN,
SERVICE_CUSTOM_FUNCTION,
handle_custom_function,
schema=SERVICE_SCHEMA)
return True return True

View File

@ -0,0 +1,15 @@
# Describes the format for available Envisalink services.
invoke_custom_function:
description: >
Allows users with DSC panels to trigger a PGM output (1-4).
Note that you need to specify the alarm panel's "code" parameter for this to work.
fields:
partition:
description: >
The alarm panel partition to trigger the PGM output on.
Typically this is just "1".
example: "1"
pgm:
description: The PGM number to trigger on the alarm panel. This will be 1-4.
example: "2"

View File

@ -0,0 +1,30 @@
{
"config": {
"abort": {
"already_configured": "ESP ja est\u00e0 configurat"
},
"error": {
"connection_error": "No s'ha pogut connectar amb ESP. Verifica que l'arxiu YAML cont\u00e9 la l\u00ednia 'api:'.",
"invalid_password": "Contrasenya inv\u00e0lida!",
"resolve_error": "No s'ha pogut trobar l'adre\u00e7a de l'ESP. Si l'error persisteix, configura una adre\u00e7a IP est\u00e0tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
"step": {
"authenticate": {
"data": {
"password": "Contrasenya"
},
"description": "Introdueix la contrasenya que has posat en la teva configuraci\u00f3.",
"title": "Introdueix la contrasenya"
},
"user": {
"data": {
"host": "Amfitri\u00f3",
"port": "Port"
},
"description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu node [ESPHome](https://esphomelib.com/).",
"title": "ESPHome"
}
},
"title": "ESPHome"
}
}

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