mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
commit
70a8cac19d
36
.coveragerc
36
.coveragerc
@ -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
|
||||||
|
1
.github/ISSUE_TEMPLATE.md
vendored
1
.github/ISSUE_TEMPLATE.md
vendored
@ -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!
|
||||||
|
1
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@ -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!
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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'
|
||||||
|
@ -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."""
|
||||||
|
147
homeassistant/components/air_quality/__init__.py
Normal file
147
homeassistant/components/air_quality/__init__.py
Normal 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
|
56
homeassistant/components/air_quality/demo.py
Normal file
56
homeassistant/components/air_quality/demo.py
Normal 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'
|
105
homeassistant/components/air_quality/opensensemap.py
Normal file
105
homeassistant/components/air_quality/opensensemap.py
Normal 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)
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
107
homeassistant/components/alarm_control_panel/ness_alarm.py
Normal file
107
homeassistant/components/alarm_control_panel/ness_alarm.py
Normal 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()
|
@ -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)
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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}
|
||||||
})
|
})
|
||||||
|
154
homeassistant/components/alexa/auth.py
Normal file
154
homeassistant/components/alexa/auth.py
Normal 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)
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
63
homeassistant/components/binary_sensor/esphome.py
Normal file
63
homeassistant/components/binary_sensor/esphome.py
Normal 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
|
@ -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):
|
||||||
|
@ -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'
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
81
homeassistant/components/binary_sensor/ness_alarm.py
Normal file
81
homeassistant/components/binary_sensor/ness_alarm.py
Normal 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()
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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({
|
||||||
|
@ -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],
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"description": "Voleu configurar Google Cast?",
|
"description": "Vols configurar Google Cast?",
|
||||||
"title": "Google Cast"
|
"title": "Google Cast"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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": {
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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']
|
||||||
|
@ -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()
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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),
|
||||||
)
|
)
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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({
|
||||||
|
@ -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({
|
||||||
|
89
homeassistant/components/cover/esphome.py
Normal file
89
homeassistant/components/cover/esphome.py
Normal 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)
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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):
|
||||||
|
@ -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."""
|
||||||
|
@ -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')
|
|
19
homeassistant/components/daikin/.translations/ca.json
Normal file
19
homeassistant/components/daikin/.translations/ca.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
19
homeassistant/components/daikin/.translations/en.json
Normal file
19
homeassistant/components/daikin/.translations/en.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
19
homeassistant/components/daikin/.translations/ko.json
Normal file
19
homeassistant/components/daikin/.translations/ko.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
19
homeassistant/components/daikin/.translations/lb.json
Normal file
19
homeassistant/components/daikin/.translations/lb.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
19
homeassistant/components/daikin/.translations/ru.json
Normal file
19
homeassistant/components/daikin/.translations/ru.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
19
homeassistant/components/daikin/.translations/sl.json
Normal file
19
homeassistant/components/daikin/.translations/sl.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
19
homeassistant/components/daikin/.translations/zh-Hant.json
Normal file
19
homeassistant/components/daikin/.translations/zh-Hant.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
146
homeassistant/components/daikin/__init__.py
Normal file
146
homeassistant/components/daikin/__init__.py
Normal 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('_', '.'),
|
||||||
|
}
|
74
homeassistant/components/daikin/config_flow.py
Normal file
74
homeassistant/components/daikin/config_flow.py
Normal 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])
|
25
homeassistant/components/daikin/const.py
Normal file
25
homeassistant/components/daikin/const.py
Normal 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'
|
19
homeassistant/components/daikin/strings.json
Normal file
19
homeassistant/components/daikin/strings.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -28,6 +28,6 @@
|
|||||||
"title": "Extra Konfiguratiouns Optiounen fir deCONZ"
|
"title": "Extra Konfiguratiouns Optiounen fir deCONZ"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "deCONZ"
|
"title": "deCONZ Zigbee gateway"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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',
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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, {}))
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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({
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
15
homeassistant/components/envisalink/services.yaml
Normal file
15
homeassistant/components/envisalink/services.yaml
Normal 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"
|
30
homeassistant/components/esphome/.translations/ca.json
Normal file
30
homeassistant/components/esphome/.translations/ca.json
Normal 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
Loading…
x
Reference in New Issue
Block a user