Compare commits
234 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1d9704292 | |||
| 283cd80a7f | |||
| 7da8cb225f | |||
| a3a73b418a | |||
| 6fa8c2afe5 | |||
| 675fb2010d | |||
| e4c0cec7f1 | |||
| d978d58436 | |||
| 5fd9220812 | |||
| 7cd7b43d25 | |||
| c26fb9906f | |||
| 58cc3a2d7a | |||
| b8a03f1283 | |||
| 2e66898bec | |||
| 2531d54515 | |||
| 3aa08f6c91 | |||
| 7314ec7a42 | |||
| a5155a2609 | |||
| 3dbf951086 | |||
| 1bbaa00976 | |||
| a5a970709f | |||
| 185ada2354 | |||
| dcaa5fe443 | |||
| 8ea7e4bb55 | |||
| 252ee35d61 | |||
| e41b00fb4d | |||
| a05afd58e9 | |||
| 0f7c35859b | |||
| 15c3ea0d86 | |||
| 392588e519 | |||
| 3996c609b4 | |||
| c44397e257 | |||
| 5851944f80 | |||
| 77fb1baeb6 | |||
| 94dcf36d7c | |||
| ced642c862 | |||
| 71e06c566f | |||
| fd97c23cde | |||
| 2219dcaee5 | |||
| 5f24cc229d | |||
| 811f6b4092 | |||
| 6ccf039c95 | |||
| f2c605ba1b | |||
| c54b2c43d4 | |||
| 8a3f8457e8 | |||
| bda6d2c696 | |||
| 840072e92f | |||
| 258ad8fc16 | |||
| e2866a1339 | |||
| 308152f48c | |||
| c2bbc2f74e | |||
| 73a15ddd64 | |||
| f3fc571cd5 | |||
| 78bb0da5a0 | |||
| 515982a692 | |||
| 7b0628421d | |||
| 04bed51277 | |||
| a7bce5f9e6 | |||
| 26c98512c8 | |||
| 5de39fd118 | |||
| 175b4ae5e0 | |||
| 0100af0fa6 | |||
| 1c8253f762 | |||
| 4126b8bd13 | |||
| 9c603d932d | |||
| 20f3e3dcf9 | |||
| 371d1cc872 | |||
| 3430c1c8bc | |||
| f5dee2c27d | |||
| 5db55b306e | |||
| ba5e8d133d | |||
| c94b3a7bf9 | |||
| 28d312803b | |||
| 07cb7b3d54 | |||
| 5b453ca53a | |||
| b21bfe50d7 | |||
| 411c9620c1 | |||
| 89d6784fa0 | |||
| d90801f6dd | |||
| 2c8967d0d5 | |||
| f5ffef3f72 | |||
| c8da95c1e8 | |||
| fdf2d24a8b | |||
| 05192e678e | |||
| 29b62f814f | |||
| c9fc3fae6e | |||
| 90f9a6bc0a | |||
| 1afdde61e8 | |||
| 2a8620f806 | |||
| 804d06d0d3 | |||
| 202d4d8105 | |||
| 04dccb4246 | |||
| 7f5c4cd1e5 | |||
| c84a099b0f | |||
| 659dc2e557 | |||
| 10c0744c4a | |||
| 51ff6009a3 | |||
| 6d01838632 | |||
| 31f189da82 | |||
| c7ecebfd07 | |||
| cc1979691e | |||
| e2fc9669f0 | |||
| 7307ab878a | |||
| 160c7fc685 | |||
| 313a9e3984 | |||
| ba310d3bd1 | |||
| 3f2eba0932 | |||
| 2d72cff575 | |||
| 3065575777 | |||
| 74bfcde814 | |||
| c539b5c12b | |||
| d2d876945b | |||
| 2defb85fb2 | |||
| 7036a7845c | |||
| c44972c2c9 | |||
| fc7ffba9ae | |||
| 5ec5552803 | |||
| d1ef47384d | |||
| 3b2bf1d567 | |||
| 77d0ad1797 | |||
| 9a7089bad3 | |||
| 894200d87d | |||
| fad914de8c | |||
| 5971a7c009 | |||
| e7a5f7bcdf | |||
| 9ade8002ac | |||
| 788275da32 | |||
| 418ccc820a | |||
| e4bb8b0444 | |||
| 552abf7da5 | |||
| 9ede0f57e6 | |||
| 0b1677de6d | |||
| 968ed6ef5b | |||
| a28ac37a91 | |||
| 5ba39c849e | |||
| 984cae5310 | |||
| c3a91000ac | |||
| ed699896cb | |||
| 67828cb7a2 | |||
| 54de3d89d1 | |||
| 1b5e574a76 | |||
| e6207684bf | |||
| 7c7a5a4a15 | |||
| 5dfd60a029 | |||
| 38e1b81ff6 | |||
| 68343ac81f | |||
| 7694c31814 | |||
| db36b5cd23 | |||
| a78f5e0970 | |||
| 0889e38cb1 | |||
| f51163f803 | |||
| 639eb81aef | |||
| 8797932f80 | |||
| 8d1f6d3995 | |||
| 4defd96cd6 | |||
| 185d838803 | |||
| 713f7fa2a1 | |||
| 4cd5173ac8 | |||
| 8d5f6723ce | |||
| a55895b662 | |||
| 0af4f8903d | |||
| 836b528bd3 | |||
| 274e4449ea | |||
| acb6b7c68d | |||
| 7d281fd224 | |||
| 60342b4738 | |||
| 99c1c9472a | |||
| d816ff26ad | |||
| e22ec28bce | |||
| bb37294047 | |||
| de4a4fe71a | |||
| 5f445b4a13 | |||
| 10e8aea46b | |||
| 76c7eef7d8 | |||
| 214c92d787 | |||
| f2551c08af | |||
| 3a0e38aa73 | |||
| 56f9ccb877 | |||
| f76436f326 | |||
| 4aafcfa478 | |||
| 8673e53940 | |||
| ebc7ade591 | |||
| 7de73e9ef7 | |||
| 0b58d5405e | |||
| 33c906c20a | |||
| 81a00bf3f1 | |||
| b8d737c0cc | |||
| ee28b439b3 | |||
| aa8dd8fbdd | |||
| 3e0eb8763f | |||
| 0687a457b1 | |||
| 38071501b4 | |||
| 5d800c1d51 | |||
| 75559cb81f | |||
| 0de6a37822 | |||
| 6505019701 | |||
| e76e9e0966 | |||
| bd71a33ba8 | |||
| 0ccff6c03e | |||
| 3509ecf07f | |||
| 308b822832 | |||
| d986b8f4c2 | |||
| e6892a4077 | |||
| 422be25d22 | |||
| 0ae1f85f9f | |||
| 8a89643338 | |||
| 10e3c00f07 | |||
| cc18b5af3d | |||
| 924290adb0 | |||
| f9c22b0e61 | |||
| 2533b49aef | |||
| f6a701e843 | |||
| bd039b8c53 | |||
| de48d42f33 | |||
| bf315da8df | |||
| 654f6892f9 | |||
| cd3f0f8f96 | |||
| 499d54c8fc | |||
| 5629157740 | |||
| c367021aa4 | |||
| 8fdd9712e6 | |||
| f47de06f02 | |||
| 7062c2b257 | |||
| ae5fca1ec9 | |||
| 8605098ea0 | |||
| 21bf089b17 | |||
| c73338bf3e | |||
| c537770786 | |||
| 493353e4de | |||
| f4d464c008 | |||
| 0d3fa59d77 | |||
| 56083c0c64 | |||
| 8775c54d29 | |||
| 044b96e3cd |
+28
-6
@@ -52,6 +52,9 @@ omit =
|
||||
|
||||
homeassistant/components/digital_ocean.py
|
||||
homeassistant/components/*/digital_ocean.py
|
||||
|
||||
homeassistant/components/doorbird.py
|
||||
homeassistant/components/*/doorbird.py
|
||||
|
||||
homeassistant/components/dweet.py
|
||||
homeassistant/components/*/dweet.py
|
||||
@@ -158,6 +161,9 @@ omit =
|
||||
homeassistant/components/rpi_pfio.py
|
||||
homeassistant/components/*/rpi_pfio.py
|
||||
|
||||
homeassistant/components/satel_integra.py
|
||||
homeassistant/components/*/satel_integra.py
|
||||
|
||||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
@@ -170,6 +176,9 @@ omit =
|
||||
homeassistant/components/tellstick.py
|
||||
homeassistant/components/*/tellstick.py
|
||||
|
||||
homeassistant/components/tesla.py
|
||||
homeassistant/components/*/tesla.py
|
||||
|
||||
homeassistant/components/*/thinkingcleaner.py
|
||||
|
||||
homeassistant/components/tradfri.py
|
||||
@@ -205,12 +214,12 @@ omit =
|
||||
homeassistant/components/wink.py
|
||||
homeassistant/components/*/wink.py
|
||||
|
||||
homeassistant/components/xiaomi.py
|
||||
homeassistant/components/binary_sensor/xiaomi.py
|
||||
homeassistant/components/cover/xiaomi.py
|
||||
homeassistant/components/light/xiaomi.py
|
||||
homeassistant/components/sensor/xiaomi.py
|
||||
homeassistant/components/switch/xiaomi.py
|
||||
homeassistant/components/xiaomi_aqara.py
|
||||
homeassistant/components/binary_sensor/xiaomi_aqara.py
|
||||
homeassistant/components/cover/xiaomi_aqara.py
|
||||
homeassistant/components/light/xiaomi_aqara.py
|
||||
homeassistant/components/sensor/xiaomi_aqara.py
|
||||
homeassistant/components/switch/xiaomi_aqara.py
|
||||
|
||||
homeassistant/components/zabbix.py
|
||||
homeassistant/components/*/zabbix.py
|
||||
@@ -244,6 +253,7 @@ omit =
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/binary_sensor/tapsaff.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/calendar/todoist.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
@@ -280,6 +290,7 @@ omit =
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/huawei_router.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/keenetic_ndms2.py
|
||||
homeassistant/components/device_tracker/linksys_ap.py
|
||||
homeassistant/components/device_tracker/linksys_smart.py
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
@@ -328,6 +339,7 @@ omit =
|
||||
homeassistant/components/light/tplink.py
|
||||
homeassistant/components/light/tradfri.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/xiaomi_miio.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
homeassistant/components/light/yeelightsunflower.py
|
||||
homeassistant/components/light/zengge.py
|
||||
@@ -380,6 +392,8 @@ omit =
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/media_player/yamaha_musiccast.py
|
||||
homeassistant/components/mycroft.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
@@ -397,6 +411,7 @@ omit =
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
homeassistant/components/notify/matrix.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/mycroft.py
|
||||
homeassistant/components/notify/nfandroidtv.py
|
||||
homeassistant/components/notify/nma.py
|
||||
homeassistant/components/notify/prowl.py
|
||||
@@ -420,6 +435,7 @@ omit =
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/airvisual.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
@@ -445,6 +461,7 @@ omit =
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/dwd_weather_warnings.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
homeassistant/components/sensor/eddystone_temperature.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
@@ -480,6 +497,7 @@ omit =
|
||||
homeassistant/components/sensor/metoffice.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/modem_callerid.py
|
||||
homeassistant/components/sensor/mopar.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
homeassistant/components/sensor/mvglive.py
|
||||
homeassistant/components/sensor/netdata.py
|
||||
@@ -517,6 +535,7 @@ omit =
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/synologydsm.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/tank_utility.py
|
||||
homeassistant/components/sensor/ted5000.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
@@ -529,6 +548,7 @@ omit =
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/sensor/worldtidesinfo.py
|
||||
homeassistant/components/sensor/worxlandroid.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/sensor/zamg.py
|
||||
@@ -554,6 +574,7 @@ omit =
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/telnet.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
@@ -570,6 +591,7 @@ omit =
|
||||
homeassistant/components/weather/zamg.py
|
||||
homeassistant/components/zeroconf.py
|
||||
homeassistant/components/zwave/util.py
|
||||
homeassistant/components/vacuum/mqtt.py
|
||||
|
||||
|
||||
[report]
|
||||
|
||||
@@ -33,10 +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
|
||||
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
||||
:target: https://discord.gg/c5DvZ4e
|
||||
.. |Join the chat at https://gitter.im/home-assistant/home-assistant| image:: https://img.shields.io/badge/gitter-general-blue.svg
|
||||
:target: https://gitter.im/home-assistant/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs| image:: https://img.shields.io/badge/gitter-development-yellowgreen.svg
|
||||
:target: https://gitter.im/home-assistant/home-assistant/devs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
|
||||
:target: https://home-assistant.io/demo/
|
||||
.. |screenshot-components| image:: https://raw.github.com/home-assistant/home-assistant/dev/docs/screenshot-components.png
|
||||
|
||||
@@ -126,6 +126,12 @@ def get_arguments() -> argparse.Namespace:
|
||||
type=int,
|
||||
default=None,
|
||||
help='Enables daily log rotation and keeps up to the specified days')
|
||||
parser.add_argument(
|
||||
'--log-file',
|
||||
type=str,
|
||||
default=None,
|
||||
help='Log file to write to. If not set, CONFIG/home-assistant.log '
|
||||
'is used')
|
||||
parser.add_argument(
|
||||
'--runner',
|
||||
action='store_true',
|
||||
@@ -256,13 +262,14 @@ def setup_and_run_hass(config_dir: str,
|
||||
}
|
||||
hass = bootstrap.from_config_dict(
|
||||
config, config_dir=config_dir, verbose=args.verbose,
|
||||
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days)
|
||||
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
|
||||
log_file=args.log_file)
|
||||
else:
|
||||
config_file = ensure_config_file(config_dir)
|
||||
print('Config directory:', config_dir)
|
||||
hass = bootstrap.from_config_file(
|
||||
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
|
||||
log_rotate_days=args.log_rotate_days)
|
||||
log_rotate_days=args.log_rotate_days, log_file=args.log_file)
|
||||
|
||||
if hass is None:
|
||||
return None
|
||||
|
||||
+26
-11
@@ -27,6 +27,10 @@ from homeassistant.helpers.signal import async_register_signal_handling
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
|
||||
# hass.data key for logging information.
|
||||
DATA_LOGGING = 'logging'
|
||||
|
||||
FIRST_INIT_COMPONENT = set((
|
||||
'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction',
|
||||
'frontend', 'history'))
|
||||
@@ -38,7 +42,8 @@ def from_config_dict(config: Dict[str, Any],
|
||||
enable_log: bool=True,
|
||||
verbose: bool=False,
|
||||
skip_pip: bool=False,
|
||||
log_rotate_days: Any=None) \
|
||||
log_rotate_days: Any=None,
|
||||
log_file: Any=None) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
@@ -56,7 +61,7 @@ def from_config_dict(config: Dict[str, Any],
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_dict(
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days)
|
||||
log_rotate_days, log_file)
|
||||
)
|
||||
|
||||
return hass
|
||||
@@ -69,7 +74,8 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
enable_log: bool=True,
|
||||
verbose: bool=False,
|
||||
skip_pip: bool=False,
|
||||
log_rotate_days: Any=None) \
|
||||
log_rotate_days: Any=None,
|
||||
log_file: Any=None) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
@@ -88,7 +94,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
if enable_log:
|
||||
async_enable_logging(hass, verbose, log_rotate_days)
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file)
|
||||
|
||||
hass.config.skip_pip = skip_pip
|
||||
if skip_pip:
|
||||
@@ -153,7 +159,8 @@ def from_config_file(config_path: str,
|
||||
hass: Optional[core.HomeAssistant]=None,
|
||||
verbose: bool=False,
|
||||
skip_pip: bool=True,
|
||||
log_rotate_days: Any=None):
|
||||
log_rotate_days: Any=None,
|
||||
log_file: Any=None):
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter if given,
|
||||
@@ -165,7 +172,7 @@ def from_config_file(config_path: str,
|
||||
# run task
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_file(
|
||||
config_path, hass, verbose, skip_pip, log_rotate_days)
|
||||
config_path, hass, verbose, skip_pip, log_rotate_days, log_file)
|
||||
)
|
||||
|
||||
return hass
|
||||
@@ -176,7 +183,8 @@ def async_from_config_file(config_path: str,
|
||||
hass: core.HomeAssistant,
|
||||
verbose: bool=False,
|
||||
skip_pip: bool=True,
|
||||
log_rotate_days: Any=None):
|
||||
log_rotate_days: Any=None,
|
||||
log_file: Any=None):
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter.
|
||||
@@ -187,7 +195,7 @@ def async_from_config_file(config_path: str,
|
||||
hass.config.config_dir = config_dir
|
||||
yield from async_mount_local_lib_path(config_dir, hass.loop)
|
||||
|
||||
async_enable_logging(hass, verbose, log_rotate_days)
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file)
|
||||
|
||||
try:
|
||||
config_dict = yield from hass.async_add_job(
|
||||
@@ -205,7 +213,7 @@ def async_from_config_file(config_path: str,
|
||||
|
||||
@core.callback
|
||||
def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
log_rotate_days=None) -> None:
|
||||
log_rotate_days=None, log_file=None) -> None:
|
||||
"""Set up the logging.
|
||||
|
||||
This method must be run in the event loop.
|
||||
@@ -239,13 +247,18 @@ def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
pass
|
||||
|
||||
# Log errors to a file if we have write access to file or config dir
|
||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
if log_file is None:
|
||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
else:
|
||||
err_log_path = os.path.abspath(log_file)
|
||||
|
||||
err_path_exists = os.path.isfile(err_log_path)
|
||||
err_dir = os.path.dirname(err_log_path)
|
||||
|
||||
# Check if we can write to the error log if it exists or that
|
||||
# we can create files in the containing directory if not.
|
||||
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \
|
||||
(not err_path_exists and os.access(hass.config.config_dir, os.W_OK)):
|
||||
(not err_path_exists and os.access(err_dir, os.W_OK)):
|
||||
|
||||
if log_rotate_days:
|
||||
err_handler = logging.handlers.TimedRotatingFileHandler(
|
||||
@@ -272,6 +285,8 @@ def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
logger.addHandler(async_handler)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Save the log file location for access by other components.
|
||||
hass.data[DATA_LOGGING] = err_log_path
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Unable to setup error log %s (access denied)", err_log_path)
|
||||
|
||||
@@ -101,6 +101,12 @@ def reload_core_config(hass):
|
||||
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_reload_core_config(hass):
|
||||
"""Reload the core config."""
|
||||
yield from hass.services.async_call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up general services related to Home Assistant."""
|
||||
|
||||
@@ -4,52 +4,140 @@ This component provides basic support for Abode Home Security system.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/abode/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from functools import partial
|
||||
from os import path
|
||||
|
||||
import voluptuous as vol
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME,
|
||||
ATTR_ENTITY_ID, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_EXCLUDE, CONF_NAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_HOMEASSISTANT_START)
|
||||
|
||||
REQUIREMENTS = ['abodepy==0.7.1']
|
||||
REQUIREMENTS = ['abodepy==0.11.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ATTRIBUTION = "Data provided by goabode.com"
|
||||
CONF_LIGHTS = "lights"
|
||||
CONF_POLLING = "polling"
|
||||
|
||||
DOMAIN = 'abode'
|
||||
DEFAULT_NAME = 'Abode'
|
||||
DATA_ABODE = 'data_abode'
|
||||
DEFAULT_ENTITY_NAMESPACE = 'abode'
|
||||
|
||||
NOTIFICATION_ID = 'abode_notification'
|
||||
NOTIFICATION_TITLE = 'Abode Security Setup'
|
||||
|
||||
EVENT_ABODE_ALARM = 'abode_alarm'
|
||||
EVENT_ABODE_ALARM_END = 'abode_alarm_end'
|
||||
EVENT_ABODE_AUTOMATION = 'abode_automation'
|
||||
EVENT_ABODE_FAULT = 'abode_panel_fault'
|
||||
EVENT_ABODE_RESTORE = 'abode_panel_restore'
|
||||
|
||||
SERVICE_SETTINGS = 'change_setting'
|
||||
SERVICE_CAPTURE_IMAGE = 'capture_image'
|
||||
SERVICE_TRIGGER = 'trigger_quick_action'
|
||||
|
||||
ATTR_DEVICE_ID = 'device_id'
|
||||
ATTR_DEVICE_NAME = 'device_name'
|
||||
ATTR_DEVICE_TYPE = 'device_type'
|
||||
ATTR_EVENT_CODE = 'event_code'
|
||||
ATTR_EVENT_NAME = 'event_name'
|
||||
ATTR_EVENT_TYPE = 'event_type'
|
||||
ATTR_EVENT_UTC = 'event_utc'
|
||||
ATTR_SETTING = 'setting'
|
||||
ATTR_USER_NAME = 'user_name'
|
||||
ATTR_VALUE = 'value'
|
||||
|
||||
ABODE_DEVICE_ID_LIST_SCHEMA = vol.Schema([str])
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_POLLING, default=False): cv.boolean,
|
||||
vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA,
|
||||
vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
CHANGE_SETTING_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_SETTING): cv.string,
|
||||
vol.Required(ATTR_VALUE): cv.string
|
||||
})
|
||||
|
||||
CAPTURE_IMAGE_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
})
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
})
|
||||
|
||||
ABODE_PLATFORMS = [
|
||||
'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover',
|
||||
'camera', 'light'
|
||||
]
|
||||
|
||||
|
||||
class AbodeSystem(object):
|
||||
"""Abode System class."""
|
||||
|
||||
def __init__(self, username, password, name, polling, exclude, lights):
|
||||
"""Initialize the system."""
|
||||
import abodepy
|
||||
self.abode = abodepy.Abode(username, password,
|
||||
auto_login=True,
|
||||
get_devices=True,
|
||||
get_automations=True)
|
||||
self.name = name
|
||||
self.polling = polling
|
||||
self.exclude = exclude
|
||||
self.lights = lights
|
||||
self.devices = []
|
||||
|
||||
def is_excluded(self, device):
|
||||
"""Check if a device is configured to be excluded."""
|
||||
return device.device_id in self.exclude
|
||||
|
||||
def is_automation_excluded(self, automation):
|
||||
"""Check if an automation is configured to be excluded."""
|
||||
return automation.automation_id in self.exclude
|
||||
|
||||
def is_light(self, device):
|
||||
"""Check if a switch device is configured as a light."""
|
||||
import abodepy.helpers.constants as CONST
|
||||
|
||||
return (device.generic_type == CONST.TYPE_LIGHT or
|
||||
(device.generic_type == CONST.TYPE_SWITCH and
|
||||
device.device_id in self.lights))
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up Abode component."""
|
||||
from abodepy.exceptions import AbodeException
|
||||
|
||||
conf = config[DOMAIN]
|
||||
username = conf.get(CONF_USERNAME)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
name = conf.get(CONF_NAME)
|
||||
polling = conf.get(CONF_POLLING)
|
||||
exclude = conf.get(CONF_EXCLUDE)
|
||||
lights = conf.get(CONF_LIGHTS)
|
||||
|
||||
try:
|
||||
data = AbodeData(username, password)
|
||||
hass.data[DATA_ABODE] = data
|
||||
|
||||
for component in ['binary_sensor', 'alarm_control_panel']:
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
hass.data[DOMAIN] = AbodeSystem(
|
||||
username, password, name, polling, exclude, lights)
|
||||
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
|
||||
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
@@ -58,18 +146,209 @@ def setup(hass, config):
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
setup_hass_services(hass)
|
||||
setup_hass_events(hass)
|
||||
setup_abode_events(hass)
|
||||
|
||||
for platform in ABODE_PLATFORMS:
|
||||
discovery.load_platform(hass, platform, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AbodeData:
|
||||
"""Shared Abode data."""
|
||||
def setup_hass_services(hass):
|
||||
"""Home assistant services."""
|
||||
from abodepy.exceptions import AbodeException
|
||||
|
||||
def __init__(self, username, password):
|
||||
"""Initialize Abode oject."""
|
||||
import abodepy
|
||||
def change_setting(call):
|
||||
"""Change an Abode system setting."""
|
||||
setting = call.data.get(ATTR_SETTING)
|
||||
value = call.data.get(ATTR_VALUE)
|
||||
|
||||
self.abode = abodepy.Abode(username, password)
|
||||
self.devices = self.abode.get_devices()
|
||||
try:
|
||||
hass.data[DOMAIN].abode.set_setting(setting, value)
|
||||
except AbodeException as ex:
|
||||
_LOGGER.warning(ex)
|
||||
|
||||
_LOGGER.debug("Abode Security set up with %s devices",
|
||||
len(self.devices))
|
||||
def capture_image(call):
|
||||
"""Capture a new image."""
|
||||
entity_ids = call.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
target_devices = [device for device in hass.data[DOMAIN].devices
|
||||
if device.entity_id in entity_ids]
|
||||
|
||||
for device in target_devices:
|
||||
device.capture()
|
||||
|
||||
def trigger_quick_action(call):
|
||||
"""Trigger a quick action."""
|
||||
entity_ids = call.data.get(ATTR_ENTITY_ID, None)
|
||||
|
||||
target_devices = [device for device in hass.data[DOMAIN].devices
|
||||
if device.entity_id in entity_ids]
|
||||
|
||||
for device in target_devices:
|
||||
device.trigger()
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))[DOMAIN]
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SETTINGS, change_setting,
|
||||
descriptions.get(SERVICE_SETTINGS),
|
||||
schema=CHANGE_SETTING_SCHEMA)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image,
|
||||
descriptions.get(SERVICE_CAPTURE_IMAGE),
|
||||
schema=CAPTURE_IMAGE_SCHEMA)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_TRIGGER, trigger_quick_action,
|
||||
descriptions.get(SERVICE_TRIGGER),
|
||||
schema=TRIGGER_SCHEMA)
|
||||
|
||||
|
||||
def setup_hass_events(hass):
|
||||
"""Home assistant start and stop callbacks."""
|
||||
def startup(event):
|
||||
"""Listen for push events."""
|
||||
hass.data[DOMAIN].abode.events.start()
|
||||
|
||||
def logout(event):
|
||||
"""Logout of Abode."""
|
||||
if not hass.data[DOMAIN].polling:
|
||||
hass.data[DOMAIN].abode.events.stop()
|
||||
|
||||
hass.data[DOMAIN].abode.logout()
|
||||
_LOGGER.info("Logged out of Abode")
|
||||
|
||||
if not hass.data[DOMAIN].polling:
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, startup)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout)
|
||||
|
||||
|
||||
def setup_abode_events(hass):
|
||||
"""Event callbacks."""
|
||||
import abodepy.helpers.timeline as TIMELINE
|
||||
|
||||
def event_callback(event, event_json):
|
||||
"""Handle an event callback from Abode."""
|
||||
data = {
|
||||
ATTR_DEVICE_ID: event_json.get(ATTR_DEVICE_ID, ''),
|
||||
ATTR_DEVICE_NAME: event_json.get(ATTR_DEVICE_NAME, ''),
|
||||
ATTR_DEVICE_TYPE: event_json.get(ATTR_DEVICE_TYPE, ''),
|
||||
ATTR_EVENT_CODE: event_json.get(ATTR_EVENT_CODE, ''),
|
||||
ATTR_EVENT_NAME: event_json.get(ATTR_EVENT_NAME, ''),
|
||||
ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ''),
|
||||
ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ''),
|
||||
ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ''),
|
||||
ATTR_DATE: event_json.get(ATTR_DATE, ''),
|
||||
ATTR_TIME: event_json.get(ATTR_TIME, ''),
|
||||
}
|
||||
|
||||
hass.bus.fire(event, data)
|
||||
|
||||
events = [TIMELINE.ALARM_GROUP, TIMELINE.ALARM_END_GROUP,
|
||||
TIMELINE.PANEL_FAULT_GROUP, TIMELINE.PANEL_RESTORE_GROUP,
|
||||
TIMELINE.AUTOMATION_GROUP]
|
||||
|
||||
for event in events:
|
||||
hass.data[DOMAIN].abode.events.add_event_callback(
|
||||
event,
|
||||
partial(event_callback, event))
|
||||
|
||||
|
||||
class AbodeDevice(Entity):
|
||||
"""Representation of an Abode device."""
|
||||
|
||||
def __init__(self, data, device):
|
||||
"""Initialize a sensor for Abode device."""
|
||||
self._data = data
|
||||
self._device = device
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe Abode events."""
|
||||
self.hass.async_add_job(
|
||||
self._data.abode.events.add_device_callback,
|
||||
self._device.device_id, self._update_callback
|
||||
)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return self._data.polling
|
||||
|
||||
def update(self):
|
||||
"""Update automation state."""
|
||||
self._device.refresh()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
'device_id': self._device.device_id,
|
||||
'battery_low': self._device.battery_low,
|
||||
'no_response': self._device.no_response,
|
||||
'device_type': self._device.type
|
||||
}
|
||||
|
||||
def _update_callback(self, device):
|
||||
"""Update the device state."""
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
class AbodeAutomation(Entity):
|
||||
"""Representation of an Abode automation."""
|
||||
|
||||
def __init__(self, data, automation, event=None):
|
||||
"""Initialize for Abode automation."""
|
||||
self._data = data
|
||||
self._automation = automation
|
||||
self._event = event
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe Abode events."""
|
||||
if self._event:
|
||||
self.hass.async_add_job(
|
||||
self._data.abode.events.add_event_callback,
|
||||
self._event, self._update_callback
|
||||
)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return self._data.polling
|
||||
|
||||
def update(self):
|
||||
"""Update automation state."""
|
||||
self._automation.refresh()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._automation.name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
'automation_id': self._automation.automation_id,
|
||||
'type': self._automation.type,
|
||||
'sub_type': self._automation.sub_type
|
||||
}
|
||||
|
||||
def _update_callback(self, device):
|
||||
"""Update the device state."""
|
||||
self._automation.refresh()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -6,10 +6,12 @@ https://home-assistant.io/components/alarm_control_panel.abode/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.abode import (DATA_ABODE, DEFAULT_NAME)
|
||||
from homeassistant.const import (STATE_ALARM_ARMED_AWAY,
|
||||
from homeassistant.components.abode import (
|
||||
AbodeDevice, DOMAIN as ABODE_DOMAIN, CONF_ATTRIBUTION)
|
||||
from homeassistant.components.alarm_control_panel import (AlarmControlPanel)
|
||||
from homeassistant.const import (ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
|
||||
@@ -20,29 +22,22 @@ ICON = 'mdi:security'
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a sensor for an Abode device."""
|
||||
data = hass.data.get(DATA_ABODE)
|
||||
data = hass.data[ABODE_DOMAIN]
|
||||
|
||||
add_devices([AbodeAlarm(hass, data, data.abode.get_alarm())])
|
||||
alarm_devices = [AbodeAlarm(data, data.abode.get_alarm(), data.name)]
|
||||
|
||||
data.devices.extend(alarm_devices)
|
||||
|
||||
add_devices(alarm_devices)
|
||||
|
||||
|
||||
class AbodeAlarm(alarm.AlarmControlPanel):
|
||||
class AbodeAlarm(AbodeDevice, AlarmControlPanel):
|
||||
"""An alarm_control_panel implementation for Abode."""
|
||||
|
||||
def __init__(self, hass, data, device):
|
||||
def __init__(self, data, device, name):
|
||||
"""Initialize the alarm control panel."""
|
||||
super(AbodeAlarm, self).__init__()
|
||||
self._device = device
|
||||
self._name = "{0}".format(DEFAULT_NAME)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
super().__init__(data, device)
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
@@ -52,11 +47,11 @@ class AbodeAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._device.mode == "standby":
|
||||
if self._device.is_standby:
|
||||
state = STATE_ALARM_DISARMED
|
||||
elif self._device.mode == "away":
|
||||
elif self._device.is_away:
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif self._device.mode == "home":
|
||||
elif self._device.is_home:
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
state = None
|
||||
@@ -65,18 +60,26 @@ class AbodeAlarm(alarm.AlarmControlPanel):
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._device.set_standby()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._device.set_home()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._device.set_away()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def update(self):
|
||||
"""Update the device state."""
|
||||
self._device.refresh()
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the alarm."""
|
||||
return self._name or super().name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
'device_id': self._device.device_id,
|
||||
'battery_backup': self._device.battery,
|
||||
'cellular_backup': self._device.is_cellular
|
||||
}
|
||||
|
||||
@@ -57,19 +57,19 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
if message.alarm_sounding or message.fire_alarm:
|
||||
if self._state != STATE_ALARM_TRIGGERED:
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
elif message.armed_away:
|
||||
if self._state != STATE_ALARM_ARMED_AWAY:
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
elif message.armed_home:
|
||||
if self._state != STATE_ALARM_ARMED_HOME:
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
else:
|
||||
if self._state != STATE_ALARM_DISARMED:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -5,10 +5,26 @@ For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import homeassistant.components.alarm_control_panel.manual as manual
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_TRIGGERED, CONF_PENDING_TIME)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Demo alarm control panel platform."""
|
||||
add_devices([
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10, False),
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10, False, {
|
||||
STATE_ALARM_ARMED_AWAY: {
|
||||
CONF_PENDING_TIME: 5
|
||||
},
|
||||
STATE_ALARM_ARMED_HOME: {
|
||||
CONF_PENDING_TIME: 5
|
||||
},
|
||||
STATE_ALARM_ARMED_NIGHT: {
|
||||
CONF_PENDING_TIME: 5
|
||||
},
|
||||
STATE_ALARM_TRIGGERED: {
|
||||
CONF_PENDING_TIME: 5
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
|
||||
|
||||
REQUIREMENTS = ['pythonegardia==1.0.18']
|
||||
REQUIREMENTS = ['pythonegardia==1.0.20']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,7 +29,7 @@ CONF_REPORT_SERVER_PORT = 'report_server_port'
|
||||
DEFAULT_NAME = 'Egardia'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_REPORT_SERVER_ENABLED = False
|
||||
DEFAULT_REPORT_SERVER_PORT = 85
|
||||
DEFAULT_REPORT_SERVER_PORT = 52010
|
||||
DOMAIN = 'egardia'
|
||||
|
||||
NOTIFICATION_ID = 'egardia_notification'
|
||||
@@ -154,8 +154,9 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def update(self):
|
||||
"""Update the alarm status."""
|
||||
status = self._egardiasystem.getstate()
|
||||
self.parsestatus(status)
|
||||
if not self._rs_enabled:
|
||||
status = self._egardiasystem.getstate()
|
||||
self.parsestatus(status)
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -106,7 +106,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
def _update_callback(self, partition):
|
||||
"""Update Home Assistant state, if needed."""
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
|
||||
@@ -4,6 +4,7 @@ Support for manual alarms.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.manual/
|
||||
"""
|
||||
import copy
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
@@ -24,7 +25,28 @@ DEFAULT_PENDING_TIME = 60
|
||||
DEFAULT_TRIGGER_TIME = 120
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED]
|
||||
|
||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
||||
|
||||
|
||||
def _state_validator(config):
|
||||
config = copy.deepcopy(config)
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
if CONF_PENDING_TIME not in config[state]:
|
||||
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
|
||||
|
||||
return config
|
||||
|
||||
|
||||
STATE_SETTING_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||
})
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(vol.All({
|
||||
vol.Required(CONF_PLATFORM): 'manual',
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
@@ -34,7 +56,11 @@ PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
||||
})
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA,
|
||||
}, _state_validator))
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -47,7 +73,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME),
|
||||
config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME),
|
||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER)
|
||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
||||
config
|
||||
)])
|
||||
|
||||
|
||||
@@ -61,19 +88,23 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
or disarm if `disarm_after_trigger` is true.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, pending_time,
|
||||
trigger_time, disarm_after_trigger):
|
||||
def __init__(self, hass, name, code, pending_time, trigger_time,
|
||||
disarm_after_trigger, config):
|
||||
"""Init the manual alarm panel."""
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._pending_time = datetime.timedelta(seconds=pending_time)
|
||||
self._trigger_time = datetime.timedelta(seconds=trigger_time)
|
||||
self._disarm_after_trigger = disarm_after_trigger
|
||||
self._pre_trigger_state = self._state
|
||||
self._state_ts = None
|
||||
|
||||
self._pending_time_by_state = {}
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
self._pending_time_by_state[state] = datetime.timedelta(
|
||||
seconds=config[state][CONF_PENDING_TIME])
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the plling state."""
|
||||
@@ -87,24 +118,27 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._state in (STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_NIGHT) and \
|
||||
self._pending_time and self._state_ts + self._pending_time > \
|
||||
dt_util.utcnow():
|
||||
return STATE_ALARM_PENDING
|
||||
|
||||
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
if self._state_ts + self._pending_time > dt_util.utcnow():
|
||||
if self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
elif (self._state_ts + self._pending_time +
|
||||
elif (self._state_ts + self._pending_time_by_state[self._state] +
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
return self._pre_trigger_state
|
||||
else:
|
||||
self._state = self._pre_trigger_state
|
||||
return self._state
|
||||
|
||||
if self._state in SUPPORTED_PENDING_STATES and \
|
||||
self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
|
||||
return self._state
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
return self._state_ts + pending_time > dt_util.utcnow()
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""One or more characters."""
|
||||
@@ -124,58 +158,47 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_HOME):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
self._update_state(STATE_ALARM_ARMED_HOME)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_AWAY):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
self._update_state(STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_NIGHT):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_ARMED_NIGHT
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
self._update_state(STATE_ALARM_ARMED_NIGHT)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command. No code needed."""
|
||||
self._pre_trigger_state = self._state
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
|
||||
self._update_state(STATE_ALARM_TRIGGERED)
|
||||
|
||||
def _update_state(self, state):
|
||||
self._state = state
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._trigger_time:
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
|
||||
if state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
self._state_ts + pending_time)
|
||||
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time + self._trigger_time)
|
||||
self._state_ts + self._trigger_time + pending_time)
|
||||
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
@@ -183,3 +206,13 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
state_attr = {}
|
||||
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
state_attr[ATTR_POST_PENDING_STATE] = self._state
|
||||
|
||||
return state_attr
|
||||
|
||||
@@ -87,7 +87,7 @@ class MqttAlarm(alarm.AlarmControlPanel):
|
||||
_LOGGER.warning("Received unexpected payload: %s", payload)
|
||||
return
|
||||
self._state = payload
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
return mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Support for Satel Integra alarm, using ETHM module: https://www.satel.pl/en/ .
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.satel_integra/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.satel_integra import (CONF_ARM_HOME_MODE,
|
||||
DATA_SATEL,
|
||||
SIGNAL_PANEL_MESSAGE)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['satel_integra']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up for AlarmDecoder alarm panels."""
|
||||
if not discovery_info:
|
||||
return
|
||||
|
||||
device = SatelIntegraAlarmPanel("Alarm Panel",
|
||||
discovery_info.get(CONF_ARM_HOME_MODE))
|
||||
async_add_devices([device])
|
||||
|
||||
|
||||
class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Representation of an AlarmDecoder-based alarm panel."""
|
||||
|
||||
def __init__(self, name, arm_home_mode):
|
||||
"""Initialize the alarm panel."""
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._arm_home_mode = arm_home_mode
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||
|
||||
@callback
|
||||
def _message_callback(self, message):
|
||||
|
||||
if message != self._state:
|
||||
self._state = message
|
||||
self.async_schedule_update_ha_state()
|
||||
else:
|
||||
_LOGGER.warning("Ignoring alarm status message, same state")
|
||||
|
||||
@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 '^\\d{4,6}$'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if code:
|
||||
yield from self.hass.data[DATA_SATEL].disarm(code)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if code:
|
||||
yield from self.hass.data[DATA_SATEL].arm(code)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if code:
|
||||
yield from self.hass.data[DATA_SATEL].arm(code,
|
||||
self._arm_home_mode)
|
||||
@@ -27,20 +27,20 @@ def _get_alarm_state(spc_mode):
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the SPC alarm control panel platform."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_AREAS] is None):
|
||||
return
|
||||
|
||||
entities = [SpcAlarm(hass=hass,
|
||||
area_id=area['id'],
|
||||
name=area['name'],
|
||||
state=_get_alarm_state(area['mode']))
|
||||
for area in discovery_info[ATTR_DISCOVER_AREAS]]
|
||||
devices = [SpcAlarm(hass=hass,
|
||||
area_id=area['id'],
|
||||
name=area['name'],
|
||||
state=_get_alarm_state(area['mode']))
|
||||
for area in discovery_info[ATTR_DISCOVER_AREAS]]
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class SpcAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Support for Alexa skill service end point.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alexa/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import (
|
||||
DOMAIN, CONF_UID, CONF_TITLE, CONF_AUDIO, CONF_TEXT, CONF_DISPLAY_URL)
|
||||
from . import flash_briefings, intent
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
CONF_FLASH_BRIEFINGS = 'flash_briefings'
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
CONF_FLASH_BRIEFINGS: {
|
||||
cv.string: vol.All(cv.ensure_list, [{
|
||||
vol.Optional(CONF_UID): cv.string,
|
||||
vol.Required(CONF_TITLE): cv.template,
|
||||
vol.Optional(CONF_AUDIO): cv.template,
|
||||
vol.Required(CONF_TEXT, default=""): cv.template,
|
||||
vol.Optional(CONF_DISPLAY_URL): cv.template,
|
||||
}]),
|
||||
}
|
||||
}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Activate Alexa component."""
|
||||
config = config.get(DOMAIN, {})
|
||||
flash_briefings_config = config.get(CONF_FLASH_BRIEFINGS)
|
||||
|
||||
intent.async_setup(hass)
|
||||
|
||||
if flash_briefings_config:
|
||||
flash_briefings.async_setup(hass, flash_briefings_config)
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Constants for the Alexa integration."""
|
||||
DOMAIN = 'alexa'
|
||||
|
||||
# Flash briefing constants
|
||||
CONF_UID = 'uid'
|
||||
CONF_TITLE = 'title'
|
||||
CONF_AUDIO = 'audio'
|
||||
CONF_TEXT = 'text'
|
||||
CONF_DISPLAY_URL = 'display_url'
|
||||
|
||||
ATTR_UID = 'uid'
|
||||
ATTR_UPDATE_DATE = 'updateDate'
|
||||
ATTR_TITLE_TEXT = 'titleText'
|
||||
ATTR_STREAM_URL = 'streamUrl'
|
||||
ATTR_MAIN_TEXT = 'mainText'
|
||||
ATTR_REDIRECTION_URL = 'redirectionURL'
|
||||
|
||||
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
|
||||
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Support for Alexa skill service end point.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alexa/
|
||||
"""
|
||||
import copy
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.components import http
|
||||
|
||||
from .const import (
|
||||
CONF_UID, CONF_TITLE, CONF_AUDIO, CONF_TEXT, CONF_DISPLAY_URL, ATTR_UID,
|
||||
ATTR_UPDATE_DATE, ATTR_TITLE_TEXT, ATTR_STREAM_URL, ATTR_MAIN_TEXT,
|
||||
ATTR_REDIRECTION_URL, DATE_FORMAT)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}'
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup(hass, flash_briefing_config):
|
||||
"""Activate Alexa component."""
|
||||
hass.http.register_view(
|
||||
AlexaFlashBriefingView(hass, flash_briefing_config))
|
||||
|
||||
|
||||
class AlexaFlashBriefingView(http.HomeAssistantView):
|
||||
"""Handle Alexa Flash Briefing skill requests."""
|
||||
|
||||
url = FLASH_BRIEFINGS_API_ENDPOINT
|
||||
name = 'api:alexa:flash_briefings'
|
||||
|
||||
def __init__(self, hass, flash_briefings):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__()
|
||||
self.flash_briefings = copy.deepcopy(flash_briefings)
|
||||
template.attach(hass, self.flash_briefings)
|
||||
|
||||
@callback
|
||||
def get(self, request, briefing_id):
|
||||
"""Handle Alexa Flash Briefing request."""
|
||||
_LOGGER.debug('Received Alexa flash briefing request for: %s',
|
||||
briefing_id)
|
||||
|
||||
if self.flash_briefings.get(briefing_id) is None:
|
||||
err = 'No configured Alexa flash briefing was found for: %s'
|
||||
_LOGGER.error(err, briefing_id)
|
||||
return b'', 404
|
||||
|
||||
briefing = []
|
||||
|
||||
for item in self.flash_briefings.get(briefing_id, []):
|
||||
output = {}
|
||||
if item.get(CONF_TITLE) is not None:
|
||||
if isinstance(item.get(CONF_TITLE), template.Template):
|
||||
output[ATTR_TITLE_TEXT] = item[CONF_TITLE].async_render()
|
||||
else:
|
||||
output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE)
|
||||
|
||||
if item.get(CONF_TEXT) is not None:
|
||||
if isinstance(item.get(CONF_TEXT), template.Template):
|
||||
output[ATTR_MAIN_TEXT] = item[CONF_TEXT].async_render()
|
||||
else:
|
||||
output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT)
|
||||
|
||||
uid = item.get(CONF_UID)
|
||||
if uid is None:
|
||||
uid = str(uuid.uuid4())
|
||||
output[ATTR_UID] = uid
|
||||
|
||||
if item.get(CONF_AUDIO) is not None:
|
||||
if isinstance(item.get(CONF_AUDIO), template.Template):
|
||||
output[ATTR_STREAM_URL] = item[CONF_AUDIO].async_render()
|
||||
else:
|
||||
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
|
||||
|
||||
if item.get(CONF_DISPLAY_URL) is not None:
|
||||
if isinstance(item.get(CONF_DISPLAY_URL),
|
||||
template.Template):
|
||||
output[ATTR_REDIRECTION_URL] = \
|
||||
item[CONF_DISPLAY_URL].async_render()
|
||||
else:
|
||||
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
|
||||
|
||||
output[ATTR_UPDATE_DATE] = datetime.now().strftime(DATE_FORMAT)
|
||||
|
||||
briefing.append(output)
|
||||
|
||||
return self.json(briefing)
|
||||
@@ -5,52 +5,19 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alexa/
|
||||
"""
|
||||
import asyncio
|
||||
import copy
|
||||
import enum
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import intent, template, config_validation as cv
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.components import http
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .const import DOMAIN
|
||||
|
||||
INTENTS_API_ENDPOINT = '/api/alexa'
|
||||
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}'
|
||||
|
||||
CONF_ACTION = 'action'
|
||||
CONF_CARD = 'card'
|
||||
CONF_INTENTS = 'intents'
|
||||
CONF_SPEECH = 'speech'
|
||||
|
||||
CONF_TYPE = 'type'
|
||||
CONF_TITLE = 'title'
|
||||
CONF_CONTENT = 'content'
|
||||
CONF_TEXT = 'text'
|
||||
|
||||
CONF_FLASH_BRIEFINGS = 'flash_briefings'
|
||||
CONF_UID = 'uid'
|
||||
CONF_TITLE = 'title'
|
||||
CONF_AUDIO = 'audio'
|
||||
CONF_TEXT = 'text'
|
||||
CONF_DISPLAY_URL = 'display_url'
|
||||
|
||||
ATTR_UID = 'uid'
|
||||
ATTR_UPDATE_DATE = 'updateDate'
|
||||
ATTR_TITLE_TEXT = 'titleText'
|
||||
ATTR_STREAM_URL = 'streamUrl'
|
||||
ATTR_MAIN_TEXT = 'mainText'
|
||||
ATTR_REDIRECTION_URL = 'redirectionURL'
|
||||
|
||||
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SpeechType(enum.Enum):
|
||||
@@ -73,30 +40,10 @@ class CardType(enum.Enum):
|
||||
link_account = "LinkAccount"
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
CONF_FLASH_BRIEFINGS: {
|
||||
cv.string: vol.All(cv.ensure_list, [{
|
||||
vol.Required(CONF_UID, default=str(uuid.uuid4())): cv.string,
|
||||
vol.Required(CONF_TITLE): cv.template,
|
||||
vol.Optional(CONF_AUDIO): cv.template,
|
||||
vol.Required(CONF_TEXT, default=""): cv.template,
|
||||
vol.Optional(CONF_DISPLAY_URL): cv.template,
|
||||
}]),
|
||||
}
|
||||
}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
@callback
|
||||
def async_setup(hass):
|
||||
"""Activate Alexa component."""
|
||||
flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {})
|
||||
|
||||
hass.http.register_view(AlexaIntentsView)
|
||||
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefings))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AlexaIntentsView(http.HomeAssistantView):
|
||||
@@ -255,66 +202,3 @@ class AlexaResponse(object):
|
||||
'sessionAttributes': self.session_attributes,
|
||||
'response': response,
|
||||
}
|
||||
|
||||
|
||||
class AlexaFlashBriefingView(http.HomeAssistantView):
|
||||
"""Handle Alexa Flash Briefing skill requests."""
|
||||
|
||||
url = FLASH_BRIEFINGS_API_ENDPOINT
|
||||
name = 'api:alexa:flash_briefings'
|
||||
|
||||
def __init__(self, hass, flash_briefings):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__()
|
||||
self.flash_briefings = copy.deepcopy(flash_briefings)
|
||||
template.attach(hass, self.flash_briefings)
|
||||
|
||||
@callback
|
||||
def get(self, request, briefing_id):
|
||||
"""Handle Alexa Flash Briefing request."""
|
||||
_LOGGER.debug('Received Alexa flash briefing request for: %s',
|
||||
briefing_id)
|
||||
|
||||
if self.flash_briefings.get(briefing_id) is None:
|
||||
err = 'No configured Alexa flash briefing was found for: %s'
|
||||
_LOGGER.error(err, briefing_id)
|
||||
return b'', 404
|
||||
|
||||
briefing = []
|
||||
|
||||
for item in self.flash_briefings.get(briefing_id, []):
|
||||
output = {}
|
||||
if item.get(CONF_TITLE) is not None:
|
||||
if isinstance(item.get(CONF_TITLE), template.Template):
|
||||
output[ATTR_TITLE_TEXT] = item[CONF_TITLE].async_render()
|
||||
else:
|
||||
output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE)
|
||||
|
||||
if item.get(CONF_TEXT) is not None:
|
||||
if isinstance(item.get(CONF_TEXT), template.Template):
|
||||
output[ATTR_MAIN_TEXT] = item[CONF_TEXT].async_render()
|
||||
else:
|
||||
output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT)
|
||||
|
||||
if item.get(CONF_UID) is not None:
|
||||
output[ATTR_UID] = item.get(CONF_UID)
|
||||
|
||||
if item.get(CONF_AUDIO) is not None:
|
||||
if isinstance(item.get(CONF_AUDIO), template.Template):
|
||||
output[ATTR_STREAM_URL] = item[CONF_AUDIO].async_render()
|
||||
else:
|
||||
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
|
||||
|
||||
if item.get(CONF_DISPLAY_URL) is not None:
|
||||
if isinstance(item.get(CONF_DISPLAY_URL),
|
||||
template.Template):
|
||||
output[ATTR_REDIRECTION_URL] = \
|
||||
item[CONF_DISPLAY_URL].async_render()
|
||||
else:
|
||||
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
|
||||
|
||||
output[ATTR_UPDATE_DATE] = datetime.now().strftime(DATE_FORMAT)
|
||||
|
||||
briefing.append(output)
|
||||
|
||||
return self.json(briefing)
|
||||
@@ -0,0 +1,185 @@
|
||||
"""Support for alexa Smart Home Skill API."""
|
||||
import asyncio
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||
from homeassistant.components import switch, light
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_HEADER = 'header'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_NAMESPACE = 'namespace'
|
||||
ATTR_MESSAGE_ID = 'messageId'
|
||||
ATTR_PAYLOAD = 'payload'
|
||||
ATTR_PAYLOAD_VERSION = 'payloadVersion'
|
||||
|
||||
|
||||
MAPPING_COMPONENT = {
|
||||
switch.DOMAIN: ['SWITCH', ('turnOff', 'turnOn'), None],
|
||||
light.DOMAIN: [
|
||||
'LIGHT', ('turnOff', 'turnOn'), {
|
||||
light.SUPPORT_BRIGHTNESS: 'setPercentage'
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def mapping_api_function(name):
|
||||
"""Return function pointer to api function for name.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
mapping = {
|
||||
'DiscoverAppliancesRequest': async_api_discovery,
|
||||
'TurnOnRequest': async_api_turn_on,
|
||||
'TurnOffRequest': async_api_turn_off,
|
||||
'SetPercentageRequest': async_api_set_percentage,
|
||||
}
|
||||
return mapping.get(name, None)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_message(hass, message):
|
||||
"""Handle incomming API messages."""
|
||||
assert int(message[ATTR_HEADER][ATTR_PAYLOAD_VERSION]) == 2
|
||||
|
||||
# Do we support this API request?
|
||||
funct_ref = mapping_api_function(message[ATTR_HEADER][ATTR_NAME])
|
||||
if not funct_ref:
|
||||
_LOGGER.warning(
|
||||
"Unsupported API request %s", message[ATTR_HEADER][ATTR_NAME])
|
||||
return api_error(message)
|
||||
|
||||
return (yield from funct_ref(hass, message))
|
||||
|
||||
|
||||
def api_message(name, namespace, payload=None):
|
||||
"""Create a API formated response message.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
payload = payload or {}
|
||||
return {
|
||||
ATTR_HEADER: {
|
||||
ATTR_MESSAGE_ID: uuid4(),
|
||||
ATTR_NAME: name,
|
||||
ATTR_NAMESPACE: namespace,
|
||||
ATTR_PAYLOAD_VERSION: '2',
|
||||
},
|
||||
ATTR_PAYLOAD: payload,
|
||||
}
|
||||
|
||||
|
||||
def api_error(request, exc='DriverInternalError'):
|
||||
"""Create a API formated error response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return api_message(exc, request[ATTR_HEADER][ATTR_NAMESPACE])
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_api_discovery(hass, request):
|
||||
"""Create a API formated discovery response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
discovered_appliances = []
|
||||
|
||||
for entity in hass.states.async_all():
|
||||
class_data = MAPPING_COMPONENT.get(entity.domain)
|
||||
|
||||
if not class_data:
|
||||
continue
|
||||
|
||||
appliance = {
|
||||
'actions': [],
|
||||
'applianceTypes': [class_data[0]],
|
||||
'additionalApplianceDetails': {},
|
||||
'applianceId': entity.entity_id.replace('.', '#'),
|
||||
'friendlyDescription': '',
|
||||
'friendlyName': entity.name,
|
||||
'isReachable': True,
|
||||
'manufacturerName': 'Unknown',
|
||||
'modelName': 'Unknown',
|
||||
'version': 'Unknown',
|
||||
}
|
||||
|
||||
# static actions
|
||||
if class_data[1]:
|
||||
appliance['actions'].extend(list(class_data[1]))
|
||||
|
||||
# dynamic actions
|
||||
if class_data[2]:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
for feature, action_name in class_data[2].items():
|
||||
if feature & supported > 0:
|
||||
appliance['actions'].append(action_name)
|
||||
|
||||
discovered_appliances.append(appliance)
|
||||
|
||||
return api_message(
|
||||
'DiscoverAppliancesResponse', 'Alexa.ConnectedHome.Discovery',
|
||||
payload={'discoveredAppliances': discovered_appliances})
|
||||
|
||||
|
||||
def extract_entity(funct):
|
||||
"""Decorator for extract entity object from request."""
|
||||
@asyncio.coroutine
|
||||
def async_api_entity_wrapper(hass, request):
|
||||
"""Process a turn on request."""
|
||||
entity_id = \
|
||||
request[ATTR_PAYLOAD]['appliance']['applianceId'].replace('#', '.')
|
||||
|
||||
# extract state object
|
||||
entity = hass.states.get(entity_id)
|
||||
if not entity:
|
||||
_LOGGER.error("Can't process %s for %s",
|
||||
request[ATTR_HEADER][ATTR_NAME], entity_id)
|
||||
return api_error(request)
|
||||
|
||||
return (yield from funct(hass, request, entity))
|
||||
|
||||
return async_api_entity_wrapper
|
||||
|
||||
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_turn_on(hass, request, entity):
|
||||
"""Process a turn on request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
|
||||
return api_message('TurnOnConfirmation', 'Alexa.ConnectedHome.Control')
|
||||
|
||||
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_turn_off(hass, request, entity):
|
||||
"""Process a turn off request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_OFF, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
|
||||
return api_message('TurnOffConfirmation', 'Alexa.ConnectedHome.Control')
|
||||
|
||||
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_percentage(hass, request, entity):
|
||||
"""Process a set percentage request."""
|
||||
if entity.domain == light.DOMAIN:
|
||||
brightness = request[ATTR_PAYLOAD]['percentageState']['value']
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS: brightness,
|
||||
}, blocking=True)
|
||||
else:
|
||||
return api_error(request)
|
||||
|
||||
return api_message(
|
||||
'SetPercentageConfirmation', 'Alexa.ConnectedHome.Control')
|
||||
@@ -263,7 +263,7 @@ class AndroidIPCamEntity(Entity):
|
||||
"""Update callback."""
|
||||
if self._host != host:
|
||||
return
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update)
|
||||
|
||||
@@ -13,7 +13,7 @@ import async_timeout
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant.bootstrap import ERROR_LOG_FILENAME
|
||||
from homeassistant.bootstrap import DATA_LOGGING
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
||||
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND,
|
||||
@@ -51,8 +51,9 @@ def setup(hass, config):
|
||||
hass.http.register_view(APIComponentsView)
|
||||
hass.http.register_view(APITemplateView)
|
||||
|
||||
hass.http.register_static_path(
|
||||
URL_API_ERROR_LOG, hass.config.path(ERROR_LOG_FILENAME), False)
|
||||
log_path = hass.data.get(DATA_LOGGING, None)
|
||||
if log_path:
|
||||
hass.http.register_static_path(URL_API_ERROR_LOG, log_path, False)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from typing import Union, TypeVar, Sequence
|
||||
from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_ENTITY_ID)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -45,8 +46,19 @@ NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication'
|
||||
NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification'
|
||||
NOTIFICATION_SCAN_TITLE = 'Apple TV Scan'
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
# This version of ensure_list interprets an empty dict as no value
|
||||
def ensure_list(value: Union[T, Sequence[T]]) -> Sequence[T]:
|
||||
"""Wrap value in list if it is not one."""
|
||||
if value is None or (isinstance(value, dict) and not value):
|
||||
return []
|
||||
return value if isinstance(value, list) else [value]
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
DOMAIN: vol.All(ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_LOGIN_ID): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
@@ -133,6 +145,10 @@ def async_setup(hass, config):
|
||||
"""Handler for service calls."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if service.service == SERVICE_SCAN:
|
||||
hass.async_add_job(scan_for_apple_tvs, hass)
|
||||
return
|
||||
|
||||
if entity_ids:
|
||||
devices = [device for device in hass.data[DATA_ENTITIES]
|
||||
if device.entity_id in entity_ids]
|
||||
@@ -140,16 +156,16 @@ def async_setup(hass, config):
|
||||
devices = hass.data[DATA_ENTITIES]
|
||||
|
||||
for device in devices:
|
||||
if service.service != SERVICE_AUTHENTICATE:
|
||||
continue
|
||||
|
||||
atv = device.atv
|
||||
if service.service == SERVICE_AUTHENTICATE:
|
||||
credentials = yield from atv.airplay.generate_credentials()
|
||||
yield from atv.airplay.load_credentials(credentials)
|
||||
_LOGGER.debug('Generated new credentials: %s', credentials)
|
||||
yield from atv.airplay.start_authentication()
|
||||
hass.async_add_job(request_configuration,
|
||||
hass, config, atv, credentials)
|
||||
elif service.service == SERVICE_SCAN:
|
||||
hass.async_add_job(scan_for_apple_tvs, hass)
|
||||
credentials = yield from atv.airplay.generate_credentials()
|
||||
yield from atv.airplay.load_credentials(credentials)
|
||||
_LOGGER.debug('Generated new credentials: %s', credentials)
|
||||
yield from atv.airplay.start_authentication()
|
||||
hass.async_add_job(request_configuration,
|
||||
hass, config, atv, credentials)
|
||||
|
||||
@asyncio.coroutine
|
||||
def atv_discovered(service, info):
|
||||
|
||||
@@ -12,16 +12,18 @@ import voluptuous as vol
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
CONF_BELOW, CONF_ABOVE, CONF_FOR)
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_same_state)
|
||||
from homeassistant.helpers import condition, config_validation as cv
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'numeric_state',
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
CONF_BELOW: vol.Coerce(float),
|
||||
CONF_ABOVE: vol.Coerce(float),
|
||||
vol.Optional(CONF_BELOW): vol.Coerce(float),
|
||||
vol.Optional(CONF_ABOVE): vol.Coerce(float),
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
}), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE))
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -33,15 +35,18 @@ def async_trigger(hass, config, action):
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
below = config.get(CONF_BELOW)
|
||||
above = config.get(CONF_ABOVE)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
async_remove_track_same = None
|
||||
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
def check_numeric_state(entity, from_s, to_s):
|
||||
"""Return True if they should trigger."""
|
||||
if to_s is None:
|
||||
return
|
||||
return False
|
||||
|
||||
variables = {
|
||||
'trigger': {
|
||||
@@ -55,17 +60,56 @@ def async_trigger(hass, config, action):
|
||||
# If new one doesn't match, nothing to do
|
||||
if not condition.async_numeric_state(
|
||||
hass, to_s, below, above, value_template, variables):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal async_remove_track_same
|
||||
|
||||
if not check_numeric_state(entity, from_s, to_s):
|
||||
return
|
||||
|
||||
variables = {
|
||||
'trigger': {
|
||||
'platform': 'numeric_state',
|
||||
'entity_id': entity,
|
||||
'below': below,
|
||||
'above': above,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
}
|
||||
}
|
||||
|
||||
# Only match if old didn't exist or existed but didn't match
|
||||
# Written as: skip if old one did exist and matched
|
||||
if from_s is not None and condition.async_numeric_state(
|
||||
hass, from_s, below, above, value_template, variables):
|
||||
return
|
||||
|
||||
variables['trigger']['from_state'] = from_s
|
||||
variables['trigger']['to_state'] = to_s
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
hass.async_run_job(action, variables)
|
||||
|
||||
hass.async_run_job(action, variables)
|
||||
if not time_delta:
|
||||
call_action()
|
||||
return
|
||||
|
||||
return async_track_state_change(hass, entity_id, state_automation_listener)
|
||||
async_remove_track_same = async_track_same_state(
|
||||
hass, True, time_delta, call_action, entity_ids=entity_id,
|
||||
async_check_func=check_numeric_state)
|
||||
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener)
|
||||
|
||||
@callback
|
||||
def async_remove():
|
||||
"""Remove state listeners async."""
|
||||
unsub()
|
||||
if async_remove_track_same:
|
||||
async_remove_track_same() # pylint: disable=not-callable
|
||||
|
||||
return async_remove
|
||||
|
||||
@@ -8,28 +8,23 @@ import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import MATCH_ALL, CONF_PLATFORM
|
||||
from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_point_in_utc_time)
|
||||
async_track_state_change, async_track_same_state)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONF_ENTITY_ID = 'entity_id'
|
||||
CONF_FROM = 'from'
|
||||
CONF_TO = 'to'
|
||||
CONF_FOR = 'for'
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(
|
||||
vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'state',
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
# These are str on purpose. Want to catch YAML conversions
|
||||
CONF_FROM: str,
|
||||
CONF_TO: str,
|
||||
CONF_FOR: vol.All(cv.time_period, cv.positive_timedelta),
|
||||
}),
|
||||
cv.key_dependency(CONF_FOR, CONF_TO),
|
||||
)
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'state',
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
# These are str on purpose. Want to catch YAML conversions
|
||||
vol.Optional(CONF_FROM): str,
|
||||
vol.Optional(CONF_TO): str,
|
||||
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
}), cv.key_dependency(CONF_FOR, CONF_TO))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -39,28 +34,15 @@ def async_trigger(hass, config, action):
|
||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO, MATCH_ALL)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
async_remove_state_for_cancel = None
|
||||
async_remove_state_for_listener = None
|
||||
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
|
||||
|
||||
@callback
|
||||
def clear_listener():
|
||||
"""Clear all unsub listener."""
|
||||
nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
|
||||
|
||||
# pylint: disable=not-callable
|
||||
if async_remove_state_for_listener is not None:
|
||||
async_remove_state_for_listener()
|
||||
async_remove_state_for_listener = None
|
||||
if async_remove_state_for_cancel is not None:
|
||||
async_remove_state_for_cancel()
|
||||
async_remove_state_for_cancel = None
|
||||
async_remove_track_same = None
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
|
||||
nonlocal async_remove_track_same
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
hass.async_run_job(action, {
|
||||
@@ -78,33 +60,12 @@ def async_trigger(hass, config, action):
|
||||
from_s.last_changed == to_s.last_changed):
|
||||
return
|
||||
|
||||
if time_delta is None:
|
||||
if not time_delta:
|
||||
call_action()
|
||||
return
|
||||
|
||||
@callback
|
||||
def state_for_listener(now):
|
||||
"""Fire on state changes after a delay and calls action."""
|
||||
nonlocal async_remove_state_for_listener
|
||||
async_remove_state_for_listener = None
|
||||
clear_listener()
|
||||
call_action()
|
||||
|
||||
@callback
|
||||
def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
|
||||
"""Fire on changes and cancel for listener if changed."""
|
||||
if inner_to_s.state == to_s.state:
|
||||
return
|
||||
clear_listener()
|
||||
|
||||
# cleanup previous listener
|
||||
clear_listener()
|
||||
|
||||
async_remove_state_for_listener = async_track_point_in_utc_time(
|
||||
hass, state_for_listener, dt_util.utcnow() + time_delta)
|
||||
|
||||
async_remove_state_for_cancel = async_track_state_change(
|
||||
hass, entity, state_for_cancel_listener)
|
||||
async_remove_track_same = async_track_same_state(
|
||||
hass, to_s.state, time_delta, call_action, entity_ids=entity_id)
|
||||
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||
@@ -113,6 +74,7 @@ def async_trigger(hass, config, action):
|
||||
def async_remove():
|
||||
"""Remove state listeners async."""
|
||||
unsub()
|
||||
clear_listener()
|
||||
if async_remove_track_same:
|
||||
async_remove_track_same() # pylint: disable=not-callable
|
||||
|
||||
return async_remove
|
||||
|
||||
@@ -14,7 +14,7 @@ import voluptuous as vol
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
|
||||
CONF_HOST, CONF_INCLUDE, CONF_NAME,
|
||||
CONF_PASSWORD, CONF_TRIGGER_TIME,
|
||||
CONF_PASSWORD, CONF_PORT, CONF_TRIGGER_TIME,
|
||||
CONF_USERNAME, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.components.discovery import SERVICE_AXIS
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -23,7 +23,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
|
||||
REQUIREMENTS = ['axis==8']
|
||||
REQUIREMENTS = ['axis==12']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,6 +51,7 @@ DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_PORT, default=80): cv.positive_int,
|
||||
vol.Optional(ATTR_LOCATION, default=''): cv.string,
|
||||
})
|
||||
|
||||
@@ -76,7 +77,7 @@ SERVICE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
def request_configuration(hass, name, host, serialnumber):
|
||||
def request_configuration(hass, config, name, host, serialnumber):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
|
||||
@@ -91,15 +92,15 @@ def request_configuration(hass, name, host, serialnumber):
|
||||
if CONF_NAME not in callback_data:
|
||||
callback_data[CONF_NAME] = name
|
||||
try:
|
||||
config = DEVICE_SCHEMA(callback_data)
|
||||
device_config = DEVICE_SCHEMA(callback_data)
|
||||
except vol.Invalid:
|
||||
configurator.notify_errors(request_id,
|
||||
"Bad input, please check spelling.")
|
||||
return False
|
||||
|
||||
if setup_device(hass, config):
|
||||
if setup_device(hass, config, device_config):
|
||||
config_file = _read_config(hass)
|
||||
config_file[serialnumber] = dict(config)
|
||||
config_file[serialnumber] = dict(device_config)
|
||||
del config_file[serialnumber]['hass']
|
||||
_write_config(hass, config_file)
|
||||
configurator.request_done(request_id)
|
||||
@@ -132,6 +133,9 @@ def request_configuration(hass, name, host, serialnumber):
|
||||
{'id': ATTR_LOCATION,
|
||||
'name': "Physical location of device (optional)",
|
||||
'type': 'text'},
|
||||
{'id': CONF_PORT,
|
||||
'name': "HTTP port (default=80)",
|
||||
'type': 'number'},
|
||||
{'id': CONF_TRIGGER_TIME,
|
||||
'name': "Sensor update interval (optional)",
|
||||
'type': 'number'},
|
||||
@@ -139,7 +143,7 @@ def request_configuration(hass, name, host, serialnumber):
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, base_config):
|
||||
def setup(hass, config):
|
||||
"""Common setup for Axis devices."""
|
||||
def _shutdown(call): # pylint: disable=unused-argument
|
||||
"""Stop the metadatastream on shutdown."""
|
||||
@@ -160,16 +164,17 @@ def setup(hass, base_config):
|
||||
if serialnumber in config_file:
|
||||
# Device config saved to file
|
||||
try:
|
||||
config = DEVICE_SCHEMA(config_file[serialnumber])
|
||||
config[CONF_HOST] = host
|
||||
device_config = DEVICE_SCHEMA(config_file[serialnumber])
|
||||
device_config[CONF_HOST] = host
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
|
||||
return False
|
||||
if not setup_device(hass, config):
|
||||
_LOGGER.error("Couldn\'t set up %s", config[CONF_NAME])
|
||||
if not setup_device(hass, config, device_config):
|
||||
_LOGGER.error("Couldn\'t set up %s",
|
||||
device_config[CONF_NAME])
|
||||
else:
|
||||
# New device, create configuration request for UI
|
||||
request_configuration(hass, name, host, serialnumber)
|
||||
request_configuration(hass, config, name, host, serialnumber)
|
||||
else:
|
||||
# Device already registered, but on a different IP
|
||||
device = AXIS_DEVICES[serialnumber]
|
||||
@@ -181,13 +186,13 @@ def setup(hass, base_config):
|
||||
# Register discovery service
|
||||
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
|
||||
|
||||
if DOMAIN in base_config:
|
||||
for device in base_config[DOMAIN]:
|
||||
config = base_config[DOMAIN][device]
|
||||
if CONF_NAME not in config:
|
||||
config[CONF_NAME] = device
|
||||
if not setup_device(hass, config):
|
||||
_LOGGER.error("Couldn\'t set up %s", config[CONF_NAME])
|
||||
if DOMAIN in config:
|
||||
for device in config[DOMAIN]:
|
||||
device_config = config[DOMAIN][device]
|
||||
if CONF_NAME not in device_config:
|
||||
device_config[CONF_NAME] = device
|
||||
if not setup_device(hass, config, device_config):
|
||||
_LOGGER.error("Couldn\'t set up %s", device_config[CONF_NAME])
|
||||
|
||||
# Services to communicate with device.
|
||||
descriptions = load_yaml_config_file(
|
||||
@@ -215,20 +220,20 @@ def setup(hass, base_config):
|
||||
return True
|
||||
|
||||
|
||||
def setup_device(hass, config):
|
||||
def setup_device(hass, config, device_config):
|
||||
"""Set up device."""
|
||||
from axis import AxisDevice
|
||||
|
||||
config['hass'] = hass
|
||||
device = AxisDevice(config) # Initialize device
|
||||
device_config['hass'] = hass
|
||||
device = AxisDevice(device_config) # Initialize device
|
||||
enable_metadatastream = False
|
||||
|
||||
if device.serial_number is None:
|
||||
# If there is no serial number a connection could not be made
|
||||
_LOGGER.error("Couldn\'t connect to %s", config[CONF_HOST])
|
||||
_LOGGER.error("Couldn\'t connect to %s", device_config[CONF_HOST])
|
||||
return False
|
||||
|
||||
for component in config[CONF_INCLUDE]:
|
||||
for component in device_config[CONF_INCLUDE]:
|
||||
if component in EVENT_TYPES:
|
||||
# Sensors are created by device calling event_initialized
|
||||
# when receiving initialize messages on metadatastream
|
||||
@@ -236,7 +241,18 @@ def setup_device(hass, config):
|
||||
if not enable_metadatastream:
|
||||
enable_metadatastream = True
|
||||
else:
|
||||
discovery.load_platform(hass, component, DOMAIN, config)
|
||||
camera_config = {
|
||||
CONF_HOST: device_config[CONF_HOST],
|
||||
CONF_NAME: device_config[CONF_NAME],
|
||||
CONF_PORT: device_config[CONF_PORT],
|
||||
CONF_USERNAME: device_config[CONF_USERNAME],
|
||||
CONF_PASSWORD: device_config[CONF_PASSWORD]
|
||||
}
|
||||
discovery.load_platform(hass,
|
||||
component,
|
||||
DOMAIN,
|
||||
camera_config,
|
||||
config)
|
||||
|
||||
if enable_metadatastream:
|
||||
device.initialize_new_event = event_initialized
|
||||
|
||||
@@ -6,76 +6,69 @@ https://home-assistant.io/components/binary_sensor.abode/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.abode import (CONF_ATTRIBUTION, DATA_ABODE)
|
||||
from homeassistant.const import (ATTR_ATTRIBUTION)
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||
from homeassistant.components.abode import (AbodeDevice, AbodeAutomation,
|
||||
DOMAIN as ABODE_DOMAIN)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Sensor types: Name, device_class
|
||||
SENSOR_TYPES = {
|
||||
'Door Contact': 'opening',
|
||||
'Motion Camera': 'motion',
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a sensor for an Abode device."""
|
||||
data = hass.data.get(DATA_ABODE)
|
||||
import abodepy.helpers.constants as CONST
|
||||
import abodepy.helpers.timeline as TIMELINE
|
||||
|
||||
sensors = []
|
||||
for sensor in data.devices:
|
||||
_LOGGER.debug('Sensor type %s', sensor.type)
|
||||
if sensor.type in ['Door Contact', 'Motion Camera']:
|
||||
sensors.append(AbodeBinarySensor(hass, data, sensor))
|
||||
data = hass.data[ABODE_DOMAIN]
|
||||
|
||||
_LOGGER.debug('Adding %d sensors', len(sensors))
|
||||
add_devices(sensors)
|
||||
device_types = [CONST.TYPE_CONNECTIVITY, CONST.TYPE_MOISTURE,
|
||||
CONST.TYPE_MOTION, CONST.TYPE_OCCUPANCY,
|
||||
CONST.TYPE_OPENING]
|
||||
|
||||
devices = []
|
||||
for device in data.abode.get_devices(generic_type=device_types):
|
||||
if data.is_excluded(device):
|
||||
continue
|
||||
|
||||
devices.append(AbodeBinarySensor(data, device))
|
||||
|
||||
for automation in data.abode.get_automations(
|
||||
generic_type=CONST.TYPE_QUICK_ACTION):
|
||||
if data.is_automation_excluded(automation):
|
||||
continue
|
||||
|
||||
devices.append(AbodeQuickActionBinarySensor(
|
||||
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP))
|
||||
|
||||
data.devices.extend(devices)
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class AbodeBinarySensor(BinarySensorDevice):
|
||||
class AbodeBinarySensor(AbodeDevice, BinarySensorDevice):
|
||||
"""A binary sensor implementation for Abode device."""
|
||||
|
||||
def __init__(self, hass, data, device):
|
||||
"""Initialize a sensor for Abode device."""
|
||||
super(AbodeBinarySensor, self).__init__()
|
||||
self._device = device
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return "{0} {1}".format(self._device.type, self._device.name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
if self._device.type == 'Door Contact':
|
||||
return self._device.status != 'Closed'
|
||||
elif self._device.type == 'Motion Camera':
|
||||
return self._device.get_value('motion_event') == '1'
|
||||
return self._device.is_on
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the binary sensor."""
|
||||
return SENSOR_TYPES.get(self._device.type)
|
||||
return self._device.generic_type
|
||||
|
||||
|
||||
class AbodeQuickActionBinarySensor(AbodeAutomation, BinarySensorDevice):
|
||||
"""A binary sensor implementation for Abode quick action automations."""
|
||||
|
||||
def trigger(self):
|
||||
"""Trigger a quick automation."""
|
||||
self._automation.trigger()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {}
|
||||
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
|
||||
attrs['device_id'] = self._device.device_id
|
||||
attrs['battery_low'] = self._device.battery_low
|
||||
|
||||
return attrs
|
||||
|
||||
def update(self):
|
||||
"""Update the device state."""
|
||||
self._device.refresh()
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self._automation.is_active
|
||||
|
||||
@@ -102,11 +102,11 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self._state = 1
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def _restore_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self._state = 0
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
Use Bayesian Inference to trigger a binary sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.bayesian/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_ABOVE, CONF_BELOW, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME,
|
||||
CONF_PLATFORM, CONF_STATE, STATE_UNKNOWN)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_OBSERVATIONS = 'observations'
|
||||
CONF_PRIOR = 'prior'
|
||||
CONF_PROBABILITY_THRESHOLD = 'probability_threshold'
|
||||
CONF_P_GIVEN_F = 'prob_given_false'
|
||||
CONF_P_GIVEN_T = 'prob_given_true'
|
||||
CONF_TO_STATE = 'to_state'
|
||||
|
||||
DEFAULT_NAME = 'BayesianBinary'
|
||||
|
||||
NUMERIC_STATE_SCHEMA = vol.Schema({
|
||||
CONF_PLATFORM: 'numeric_state',
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(CONF_ABOVE): vol.Coerce(float),
|
||||
vol.Optional(CONF_BELOW): vol.Coerce(float),
|
||||
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
|
||||
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float)
|
||||
}, required=True)
|
||||
|
||||
STATE_SCHEMA = vol.Schema({
|
||||
CONF_PLATFORM: CONF_STATE,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_TO_STATE): cv.string,
|
||||
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
|
||||
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float)
|
||||
}, required=True)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME):
|
||||
cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): cv.string,
|
||||
vol.Required(CONF_OBSERVATIONS): vol.Schema(
|
||||
vol.All(cv.ensure_list, [vol.Any(NUMERIC_STATE_SCHEMA,
|
||||
STATE_SCHEMA)])
|
||||
),
|
||||
vol.Required(CONF_PRIOR): vol.Coerce(float),
|
||||
vol.Optional(CONF_PROBABILITY_THRESHOLD):
|
||||
vol.Coerce(float),
|
||||
})
|
||||
|
||||
|
||||
def update_probability(prior, prob_true, prob_false):
|
||||
"""Update probability using Bayes' rule."""
|
||||
numerator = prob_true * prior
|
||||
denominator = numerator + prob_false * (1 - prior)
|
||||
|
||||
probability = numerator / denominator
|
||||
return probability
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Threshold sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
observations = config.get(CONF_OBSERVATIONS)
|
||||
prior = config.get(CONF_PRIOR)
|
||||
probability_threshold = config.get(CONF_PROBABILITY_THRESHOLD, 0.5)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
async_add_devices([
|
||||
BayesianBinarySensor(name, prior, observations, probability_threshold,
|
||||
device_class)
|
||||
], True)
|
||||
|
||||
|
||||
class BayesianBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Bayesian sensor."""
|
||||
|
||||
def __init__(self, name, prior, observations, probability_threshold,
|
||||
device_class):
|
||||
"""Initialize the Bayesian sensor."""
|
||||
self._name = name
|
||||
self._observations = observations
|
||||
self._probability_threshold = probability_threshold
|
||||
self._device_class = device_class
|
||||
self._deviation = False
|
||||
self.prior = prior
|
||||
self.probability = prior
|
||||
|
||||
self.current_obs = OrderedDict({})
|
||||
|
||||
to_observe = set(obs['entity_id'] for obs in self._observations)
|
||||
|
||||
self.entity_obs = dict.fromkeys(to_observe, [])
|
||||
|
||||
for ind, obs in enumerate(self._observations):
|
||||
obs["id"] = ind
|
||||
self.entity_obs[obs['entity_id']].append(obs)
|
||||
|
||||
self.watchers = {
|
||||
'numeric_state': self._process_numeric_state,
|
||||
'state': self._process_state
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Call when entity about to be added to hass."""
|
||||
@callback
|
||||
# pylint: disable=invalid-name
|
||||
def async_threshold_sensor_state_listener(entity, old_state,
|
||||
new_state):
|
||||
"""Handle sensor state changes."""
|
||||
if new_state.state == STATE_UNKNOWN:
|
||||
return
|
||||
|
||||
entity_obs_list = self.entity_obs[entity]
|
||||
|
||||
for entity_obs in entity_obs_list:
|
||||
platform = entity_obs['platform']
|
||||
|
||||
self.watchers[platform](entity_obs)
|
||||
|
||||
prior = self.prior
|
||||
for obs in self.current_obs.values():
|
||||
prior = update_probability(prior, obs['prob_true'],
|
||||
obs['prob_false'])
|
||||
self.probability = prior
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state, True)
|
||||
|
||||
entities = [obs['entity_id'] for obs in self._observations]
|
||||
async_track_state_change(
|
||||
self.hass, entities, async_threshold_sensor_state_listener)
|
||||
|
||||
def _update_current_obs(self, entity_observation, should_trigger):
|
||||
"""Update current observation."""
|
||||
obs_id = entity_observation['id']
|
||||
|
||||
if should_trigger:
|
||||
prob_true = entity_observation['prob_given_true']
|
||||
prob_false = entity_observation.get(
|
||||
'prob_given_false', 1 - prob_true)
|
||||
|
||||
self.current_obs[obs_id] = {
|
||||
'prob_true': prob_true,
|
||||
'prob_false': prob_false
|
||||
}
|
||||
|
||||
else:
|
||||
self.current_obs.pop(obs_id, None)
|
||||
|
||||
def _process_numeric_state(self, entity_observation):
|
||||
"""Add entity to current_obs if numeric state conditions are met."""
|
||||
entity = entity_observation['entity_id']
|
||||
|
||||
should_trigger = condition.async_numeric_state(
|
||||
self.hass, entity,
|
||||
entity_observation.get('below'),
|
||||
entity_observation.get('above'), None, entity_observation)
|
||||
|
||||
self._update_current_obs(entity_observation, should_trigger)
|
||||
|
||||
def _process_state(self, entity_observation):
|
||||
"""Add entity to current observations if state conditions are met."""
|
||||
entity = entity_observation['entity_id']
|
||||
|
||||
should_trigger = condition.state(
|
||||
self.hass, entity, entity_observation.get('to_state'))
|
||||
|
||||
self._update_current_obs(entity_observation, should_trigger)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._deviation
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
return {
|
||||
'observations': [val for val in self.current_obs.values()],
|
||||
'probability': round(self.probability, 2),
|
||||
'probability_threshold': self._probability_threshold
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
self._deviation = bool(self.probability > self._probability_threshold)
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Support for reading binary states from a DoorBird video doorbell."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DEPENDENCIES = ['doorbird']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_MIN_UPDATE_INTERVAL = timedelta(milliseconds=250)
|
||||
|
||||
SENSOR_TYPES = {
|
||||
"doorbell": {
|
||||
"name": "Doorbell Ringing",
|
||||
"icon": {
|
||||
True: "bell-ring",
|
||||
False: "bell",
|
||||
None: "bell-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the DoorBird binary sensor component."""
|
||||
device = hass.data.get(DOORBIRD_DOMAIN)
|
||||
add_devices([DoorBirdBinarySensor(device, "doorbell")], True)
|
||||
|
||||
|
||||
class DoorBirdBinarySensor(BinarySensorDevice):
|
||||
"""A binary sensor of a DoorBird device."""
|
||||
|
||||
def __init__(self, device, sensor_type):
|
||||
"""Initialize a binary sensor on a DoorBird device."""
|
||||
self._device = device
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Get the name of the sensor."""
|
||||
return SENSOR_TYPES[self._sensor_type]["name"]
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Get an icon to display."""
|
||||
state_icon = SENSOR_TYPES[self._sensor_type]["icon"][self._state]
|
||||
return "mdi:{}".format(state_icon)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Get the state of the binary sensor."""
|
||||
return self._state
|
||||
|
||||
@Throttle(_MIN_UPDATE_INTERVAL)
|
||||
def update(self):
|
||||
"""Pull the latest value from the device."""
|
||||
self._state = self._device.doorbell_state()
|
||||
@@ -80,4 +80,4 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
def _update_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@@ -73,7 +73,7 @@ class FFmpegBinarySensor(FFmpegBase, BinarySensorDevice):
|
||||
def _async_callback(self, state):
|
||||
"""HA-FFmpeg callback for noise detection."""
|
||||
self._state = state
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.1.3']
|
||||
REQUIREMENTS = ['pyhik==0.1.4']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
@@ -47,6 +47,7 @@ DEVICE_CLASS_MAP = {
|
||||
'PIR Alarm': 'motion',
|
||||
'Face Detection': 'motion',
|
||||
'Scene Change Detection': 'motion',
|
||||
'I/O': None,
|
||||
}
|
||||
|
||||
CUSTOMIZE_SCHEMA = vol.Schema({
|
||||
|
||||
@@ -35,8 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
devices = []
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMBinarySensor(hass, conf)
|
||||
new_device.link_homematic()
|
||||
new_device = HMBinarySensor(conf)
|
||||
devices.append(new_device)
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
@@ -1,21 +1,145 @@
|
||||
"""
|
||||
Contains functionality to use a KNX group address as a binary.
|
||||
Support for KNX/IP binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.knx/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.knx import (KNXConfig, KNXGroupAddress)
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES, \
|
||||
KNXAutomation
|
||||
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, \
|
||||
BinarySensorDevice
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONF_ADDRESS = 'address'
|
||||
CONF_DEVICE_CLASS = 'device_class'
|
||||
CONF_SIGNIFICANT_BIT = 'significant_bit'
|
||||
CONF_DEFAULT_SIGNIFICANT_BIT = 1
|
||||
CONF_AUTOMATION = 'automation'
|
||||
CONF_HOOK = 'hook'
|
||||
CONF_DEFAULT_HOOK = 'on'
|
||||
CONF_COUNTER = 'counter'
|
||||
CONF_DEFAULT_COUNTER = 1
|
||||
CONF_ACTION = 'action'
|
||||
|
||||
CONF__ACTION = 'turn_off_action'
|
||||
|
||||
DEFAULT_NAME = 'KNX Binary Sensor'
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
AUTOMATION_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_HOOK, default=CONF_DEFAULT_HOOK): cv.string,
|
||||
vol.Optional(CONF_COUNTER, default=CONF_DEFAULT_COUNTER): cv.port,
|
||||
vol.Required(CONF_ACTION, default=None): cv.SCRIPT_SCHEMA
|
||||
})
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the KNX binary sensor platform."""
|
||||
add_devices([KNXSwitch(hass, KNXConfig(config))])
|
||||
AUTOMATIONS_SCHEMA = vol.All(
|
||||
cv.ensure_list,
|
||||
[AUTOMATION_SCHEMA]
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): cv.string,
|
||||
vol.Optional(CONF_SIGNIFICANT_BIT, default=CONF_DEFAULT_SIGNIFICANT_BIT):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_AUTOMATION, default=None): AUTOMATIONS_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
class KNXSwitch(KNXGroupAddress, BinarySensorDevice):
|
||||
"""Representation of a KNX binary sensor device."""
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up binary sensor(s) for KNX platform."""
|
||||
if DATA_KNX not in hass.data \
|
||||
or not hass.data[DATA_KNX].initialized:
|
||||
return False
|
||||
|
||||
pass
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, async_add_devices)
|
||||
else:
|
||||
async_add_devices_config(hass, config, async_add_devices)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_devices_discovery(hass, discovery_info, async_add_devices):
|
||||
"""Set up binary sensors for KNX platform configured via xknx.yaml."""
|
||||
entities = []
|
||||
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
device = hass.data[DATA_KNX].xknx.devices[device_name]
|
||||
entities.append(KNXBinarySensor(hass, device))
|
||||
async_add_devices(entities)
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_devices_config(hass, config, async_add_devices):
|
||||
"""Set up binary senor for KNX platform configured within plattform."""
|
||||
name = config.get(CONF_NAME)
|
||||
import xknx
|
||||
binary_sensor = xknx.devices.BinarySensor(
|
||||
hass.data[DATA_KNX].xknx,
|
||||
name=name,
|
||||
group_address=config.get(CONF_ADDRESS),
|
||||
device_class=config.get(CONF_DEVICE_CLASS),
|
||||
significant_bit=config.get(CONF_SIGNIFICANT_BIT))
|
||||
hass.data[DATA_KNX].xknx.devices.add(binary_sensor)
|
||||
|
||||
entity = KNXBinarySensor(hass, binary_sensor)
|
||||
automations = config.get(CONF_AUTOMATION)
|
||||
if automations is not None:
|
||||
for automation in automations:
|
||||
counter = automation.get(CONF_COUNTER)
|
||||
hook = automation.get(CONF_HOOK)
|
||||
action = automation.get(CONF_ACTION)
|
||||
entity.automations.append(KNXAutomation(
|
||||
hass=hass, device=binary_sensor, hook=hook,
|
||||
action=action, counter=counter))
|
||||
async_add_devices([entity])
|
||||
|
||||
|
||||
class KNXBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a KNX binary sensor."""
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialization of KNXBinarySensor."""
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
self.automations = []
|
||||
|
||||
@callback
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
def after_update_callback(device):
|
||||
"""Callback after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
yield from self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed within KNX."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self.device.device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.device.is_on()
|
||||
|
||||
@@ -16,14 +16,21 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF,
|
||||
CONF_DEVICE_CLASS)
|
||||
from homeassistant.components.mqtt import (CONF_STATE_TOPIC, CONF_QOS)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_QOS, valid_subscribe_topic)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PAYLOAD_AVAILABLE = 'payload_available'
|
||||
CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available'
|
||||
|
||||
DEFAULT_NAME = 'MQTT Binary sensor'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_PAYLOAD_AVAILABLE = 'online'
|
||||
DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline'
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
||||
@@ -31,6 +38,11 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_AVAILABILITY_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_PAYLOAD_AVAILABLE,
|
||||
default=DEFAULT_PAYLOAD_AVAILABLE): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -47,10 +59,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async_add_devices([MqttBinarySensor(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_DEVICE_CLASS),
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_PAYLOAD_ON),
|
||||
config.get(CONF_PAYLOAD_OFF),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
value_template
|
||||
)])
|
||||
|
||||
@@ -58,15 +73,20 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class MqttBinarySensor(BinarySensorDevice):
|
||||
"""Representation a binary sensor that is updated by MQTT."""
|
||||
|
||||
def __init__(self, name, state_topic, device_class, qos, payload_on,
|
||||
payload_off, value_template):
|
||||
def __init__(self, name, state_topic, availability_topic, device_class,
|
||||
qos, payload_on, payload_off, payload_available,
|
||||
payload_not_available, value_template):
|
||||
"""Initialize the MQTT binary sensor."""
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._state = None
|
||||
self._state_topic = state_topic
|
||||
self._availability_topic = availability_topic
|
||||
self._available = True if availability_topic is None else False
|
||||
self._device_class = device_class
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
self._payload_available = payload_available
|
||||
self._payload_not_available = payload_not_available
|
||||
self._qos = qos
|
||||
self._template = value_template
|
||||
|
||||
@@ -76,8 +96,8 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""Handle a new received MQTT message."""
|
||||
def state_message_received(topic, payload, qos):
|
||||
"""Handle a new received MQTT state message."""
|
||||
if self._template is not None:
|
||||
payload = self._template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
@@ -86,10 +106,25 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
elif payload == self._payload_off:
|
||||
self._state = False
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
return mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, message_received, self._qos)
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, state_message_received, self._qos)
|
||||
|
||||
@callback
|
||||
def availability_message_received(topic, payload, qos):
|
||||
"""Handle a new received MQTT availability message."""
|
||||
if payload == self._payload_available:
|
||||
self._available = True
|
||||
elif payload == self._payload_not_available:
|
||||
self._available = False
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._availability_topic is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._availability_topic,
|
||||
availability_message_received, self._qos)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -101,6 +136,11 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the binary sensor is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
|
||||
@@ -92,4 +92,4 @@ class MyStromBinarySensor(BinarySensorDevice):
|
||||
def async_on_update(self, value):
|
||||
"""Receive an update."""
|
||||
self._state = value
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@@ -103,7 +103,8 @@ class RingBinarySensor(BinarySensorDevice):
|
||||
self._data.check_alerts()
|
||||
|
||||
if self._data.alert:
|
||||
self._state = (self._sensor_type ==
|
||||
self._data.alert.get('kind'))
|
||||
if self._sensor_type == self._data.alert.get('kind') and \
|
||||
self._data.account_id == self._data.alert.get('doorbot_id'):
|
||||
self._state = True
|
||||
else:
|
||||
self._state = False
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Support for Satel Integra 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.satel_integra/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.satel_integra import (CONF_ZONES,
|
||||
CONF_ZONE_NAME,
|
||||
CONF_ZONE_TYPE,
|
||||
SIGNAL_ZONES_UPDATED)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['satel_integra']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Satel Integra binary sensor devices."""
|
||||
if not discovery_info:
|
||||
return
|
||||
|
||||
configured_zones = discovery_info[CONF_ZONES]
|
||||
|
||||
devices = []
|
||||
|
||||
for zone_num, device_config_data in configured_zones.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)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class SatelIntegraBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an Satel Integra binary sensor."""
|
||||
|
||||
def __init__(self, zone_number, zone_name, zone_type):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._zone_number = zone_number
|
||||
self._name = zone_name
|
||||
self._zone_type = zone_type
|
||||
self._state = 0
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONES_UPDATED, self._zones_updated)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon for device by its type."""
|
||||
if self._zone_type == 'smoke':
|
||||
return "mdi:fire"
|
||||
|
||||
@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._zone_type
|
||||
|
||||
@callback
|
||||
def _zones_updated(self, zones):
|
||||
"""Update the zone's state, if needed."""
|
||||
if self._zone_number in zones \
|
||||
and self._state != zones[self._zone_number]:
|
||||
self._state = zones[self._zone_number]
|
||||
self.async_schedule_update_ha_state()
|
||||
@@ -41,14 +41,14 @@ def _create_sensor(hass, zone):
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Initialize the platform."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_DEVICES] is None):
|
||||
return
|
||||
|
||||
async_add_entities(
|
||||
async_add_devices(
|
||||
_create_sensor(hass, zone)
|
||||
for zone in discovery_info[ATTR_DISCOVER_DEVICES]
|
||||
if _get_device_class(zone['type']))
|
||||
|
||||
@@ -19,16 +19,24 @@ from homeassistant.const import (
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_same_state)
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DELAY_ON = 'delay_on'
|
||||
CONF_DELAY_OFF = 'delay_off'
|
||||
|
||||
SENSOR_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DELAY_ON):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_DELAY_OFF):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -47,6 +55,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
value_template.extract_entities())
|
||||
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
||||
device_class = device_config.get(CONF_DEVICE_CLASS)
|
||||
delay_on = device_config.get(CONF_DELAY_ON)
|
||||
delay_off = device_config.get(CONF_DELAY_OFF)
|
||||
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
@@ -54,13 +64,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
sensors.append(
|
||||
BinarySensorTemplate(
|
||||
hass, device, friendly_name, device_class, value_template,
|
||||
entity_ids)
|
||||
entity_ids, delay_on, delay_off)
|
||||
)
|
||||
if not sensors:
|
||||
_LOGGER.error("No sensors added")
|
||||
return False
|
||||
|
||||
async_add_devices(sensors, True)
|
||||
async_add_devices(sensors)
|
||||
return True
|
||||
|
||||
|
||||
@@ -68,7 +78,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
"""A virtual binary sensor that triggers from another sensor."""
|
||||
|
||||
def __init__(self, hass, device, friendly_name, device_class,
|
||||
value_template, entity_ids):
|
||||
value_template, entity_ids, delay_on, delay_off):
|
||||
"""Initialize the Template binary sensor."""
|
||||
self.hass = hass
|
||||
self.entity_id = async_generate_entity_id(
|
||||
@@ -78,6 +88,8 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
self._template = value_template
|
||||
self._state = None
|
||||
self._entities = entity_ids
|
||||
self._delay_on = delay_on
|
||||
self._delay_off = delay_off
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
@@ -89,7 +101,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
@callback
|
||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||
"""Handle the target device state changes."""
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
self.async_check_state()
|
||||
|
||||
@callback
|
||||
def template_bsensor_startup(event):
|
||||
@@ -97,7 +109,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
async_track_state_change(
|
||||
self.hass, self._entities, template_bsensor_state_listener)
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
self.hass.async_add_job(self.async_check_state)
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, template_bsensor_startup)
|
||||
@@ -122,11 +134,11 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the state from the template."""
|
||||
@callback
|
||||
def _async_render(self, *args):
|
||||
"""Get the state of template."""
|
||||
try:
|
||||
self._state = self._template.async_render().lower() == 'true'
|
||||
return self._template.async_render().lower() == 'true'
|
||||
except TemplateError as ex:
|
||||
if ex.args and ex.args[0].startswith(
|
||||
"UndefinedError: 'None' has no attribute"):
|
||||
@@ -135,4 +147,29 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
"the state is unknown", self._name)
|
||||
return
|
||||
_LOGGER.error("Could not render template %s: %s", self._name, ex)
|
||||
self._state = False
|
||||
|
||||
@callback
|
||||
def async_check_state(self):
|
||||
"""Update the state from the template."""
|
||||
state = self._async_render()
|
||||
|
||||
# return if the state don't change or is invalid
|
||||
if state is None or state == self.state:
|
||||
return
|
||||
|
||||
@callback
|
||||
def set_state():
|
||||
"""Set state of template binary sensor."""
|
||||
self._state = state
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
# state without delay
|
||||
if (state and not self._delay_on) or \
|
||||
(not state and not self._delay_off):
|
||||
set_state()
|
||||
return
|
||||
|
||||
period = self._delay_on if state else self._delay_off
|
||||
async_track_same_state(
|
||||
self.hass, state, period, set_state, entity_ids=self._entities,
|
||||
async_check_func=self._async_render)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Support for Tesla binary sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.tesla/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT)
|
||||
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['tesla']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Tesla binary sensor."""
|
||||
devices = [
|
||||
TeslaBinarySensor(
|
||||
device, hass.data[TESLA_DOMAIN]['controller'], 'connectivity')
|
||||
for device in hass.data[TESLA_DOMAIN]['devices']['binary_sensor']]
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class TeslaBinarySensor(TeslaDevice, BinarySensorDevice):
|
||||
"""Implement an Tesla binary sensor for parking and charger."""
|
||||
|
||||
def __init__(self, tesla_device, controller, sensor_type):
|
||||
"""Initialisation of binary sensor."""
|
||||
super().__init__(tesla_device, controller)
|
||||
self._name = self.tesla_device.name
|
||||
self._state = False
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
|
||||
self._sensor_type = sensor_type
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this binary sensor."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the device."""
|
||||
_LOGGER.debug("Updating sensor: %s", self._name)
|
||||
self.tesla_device.update()
|
||||
self._state = self.tesla_device.get_value()
|
||||
+34
-2
@@ -1,8 +1,9 @@
|
||||
"""Support for Xiaomi binary sensors."""
|
||||
"""Support for Xiaomi aqara binary sensors."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.xiaomi import (PY_XIAOMI_GATEWAY, XiaomiDevice)
|
||||
from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY,
|
||||
XiaomiDevice)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,6 +32,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
devices.append(XiaomiDoorSensor(device, gateway))
|
||||
elif model == 'sensor_magnet.aq2':
|
||||
devices.append(XiaomiDoorSensor(device, gateway))
|
||||
elif model == 'sensor_wleak.aq1':
|
||||
devices.append(XiaomiWaterLeakSensor(device, gateway))
|
||||
elif model == 'smoke':
|
||||
devices.append(XiaomiSmokeSensor(device, gateway))
|
||||
elif model == 'natgas':
|
||||
@@ -214,6 +217,35 @@ class XiaomiDoorSensor(XiaomiBinarySensor):
|
||||
return False
|
||||
|
||||
|
||||
class XiaomiWaterLeakSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiWaterLeakSensor."""
|
||||
|
||||
def __init__(self, device, xiaomi_hub):
|
||||
"""Initialize the XiaomiWaterLeakSensor."""
|
||||
XiaomiBinarySensor.__init__(self, device, 'Water Leak Sensor',
|
||||
xiaomi_hub, 'status', 'moisture')
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
self._should_poll = False
|
||||
|
||||
value = data.get(self._data_key)
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
if value == 'leak':
|
||||
self._should_poll = True
|
||||
if self._state:
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == 'no_leak':
|
||||
if self._state:
|
||||
self._state = False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class XiaomiSmokeSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiSmokeSensor."""
|
||||
|
||||
@@ -12,6 +12,7 @@ import re
|
||||
from homeassistant.components.google import (
|
||||
CONF_OFFSET, CONF_DEVICE_ID, CONF_NAME)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.helpers.config_validation import time_period_str
|
||||
from homeassistant.helpers.entity import Entity, generate_entity_id
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
todoist:
|
||||
new_task:
|
||||
description: Create a new task and add it to a project.
|
||||
fields:
|
||||
content:
|
||||
description: The name of the task. [Required]
|
||||
example: Pick up the mail
|
||||
project:
|
||||
description: The name of the project this task should belong to. Defaults to Inbox. [Optional]
|
||||
example: Errands
|
||||
labels:
|
||||
description: Any labels that you want to apply to this task, separated by a comma. [Optional]
|
||||
example: Chores,Deliveries
|
||||
priority:
|
||||
description: The priority of this task, from 1 (normal) to 4 (urgent). [Optional]
|
||||
example: 2
|
||||
due_date:
|
||||
description: The day this task is due, in format YYYY-MM-DD. [Optional]
|
||||
example: "2018-04-01"
|
||||
@@ -0,0 +1,544 @@
|
||||
"""
|
||||
Support for Todoist task management (https://todoist.com).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/calendar.todoist/
|
||||
"""
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.calendar import (
|
||||
CalendarEventDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.google import (
|
||||
CONF_DEVICE_ID)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
CONF_ID, CONF_NAME, CONF_TOKEN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.template import DATE_STR_FORMAT
|
||||
from homeassistant.util import dt
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['todoist-python==7.0.17']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'todoist'
|
||||
|
||||
# Calendar Platform: Does this calendar event last all day?
|
||||
ALL_DAY = 'all_day'
|
||||
# Attribute: All tasks in this project
|
||||
ALL_TASKS = 'all_tasks'
|
||||
# Todoist API: "Completed" flag -- 1 if complete, else 0
|
||||
CHECKED = 'checked'
|
||||
# Attribute: Is this task complete?
|
||||
COMPLETED = 'completed'
|
||||
# Todoist API: What is this task about?
|
||||
# Service Call: What is this task about?
|
||||
CONTENT = 'content'
|
||||
# Calendar Platform: Get a calendar event's description
|
||||
DESCRIPTION = 'description'
|
||||
# Calendar Platform: Used in the '_get_date()' method
|
||||
DATETIME = 'dateTime'
|
||||
# Attribute: When is this task due?
|
||||
# Service Call: When is this task due?
|
||||
DUE_DATE = 'due_date'
|
||||
# Todoist API: Look up a task's due date
|
||||
DUE_DATE_UTC = 'due_date_utc'
|
||||
# Attribute: Is this task due today?
|
||||
DUE_TODAY = 'due_today'
|
||||
# Calendar Platform: When a calendar event ends
|
||||
END = 'end'
|
||||
# Todoist API: Look up a Project/Label/Task ID
|
||||
ID = 'id'
|
||||
# Todoist API: Fetch all labels
|
||||
# Service Call: What are the labels attached to this task?
|
||||
LABELS = 'labels'
|
||||
# Todoist API: "Name" value
|
||||
NAME = 'name'
|
||||
# Attribute: Is this task overdue?
|
||||
OVERDUE = 'overdue'
|
||||
# Attribute: What is this task's priority?
|
||||
# Todoist API: Get a task's priority
|
||||
# Service Call: What is this task's priority?
|
||||
PRIORITY = 'priority'
|
||||
# Todoist API: Look up the Project ID a Task belongs to
|
||||
PROJECT_ID = 'project_id'
|
||||
# Service Call: What Project do you want a Task added to?
|
||||
PROJECT_NAME = 'project'
|
||||
# Todoist API: Fetch all Projects
|
||||
PROJECTS = 'projects'
|
||||
# Calendar Platform: When does a calendar event start?
|
||||
START = 'start'
|
||||
# Calendar Platform: What is the next calendar event about?
|
||||
SUMMARY = 'summary'
|
||||
# Todoist API: Fetch all Tasks
|
||||
TASKS = 'items'
|
||||
|
||||
SERVICE_NEW_TASK = 'new_task'
|
||||
NEW_TASK_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONTENT): cv.string,
|
||||
vol.Optional(PROJECT_NAME, default='inbox'): vol.All(cv.string, vol.Lower),
|
||||
vol.Optional(LABELS): cv.ensure_list_csv,
|
||||
vol.Optional(PRIORITY): vol.All(vol.Coerce(int),
|
||||
vol.Range(min=1, max=4)),
|
||||
vol.Optional(DUE_DATE): cv.string
|
||||
})
|
||||
|
||||
CONF_EXTRA_PROJECTS = 'custom_projects'
|
||||
CONF_PROJECT_DUE_DATE = 'due_date_days'
|
||||
CONF_PROJECT_WHITELIST = 'include_projects'
|
||||
CONF_PROJECT_LABEL_WHITELIST = 'labels'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_TOKEN): cv.string,
|
||||
vol.Optional(CONF_EXTRA_PROJECTS, default=[]):
|
||||
vol.All(cv.ensure_list, vol.Schema([
|
||||
vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_PROJECT_DUE_DATE): vol.Coerce(int),
|
||||
vol.Optional(CONF_PROJECT_WHITELIST, default=[]):
|
||||
vol.All(cv.ensure_list, [vol.All(cv.string, vol.Lower)]),
|
||||
vol.Optional(CONF_PROJECT_LABEL_WHITELIST, default=[]):
|
||||
vol.All(cv.ensure_list, [vol.All(cv.string, vol.Lower)])
|
||||
})
|
||||
]))
|
||||
})
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Todoist platform."""
|
||||
# Check token:
|
||||
token = config.get(CONF_TOKEN)
|
||||
|
||||
# Look up IDs based on (lowercase) names.
|
||||
project_id_lookup = {}
|
||||
label_id_lookup = {}
|
||||
|
||||
from todoist.api import TodoistAPI
|
||||
api = TodoistAPI(token)
|
||||
api.sync()
|
||||
|
||||
# Setup devices:
|
||||
# Grab all projects.
|
||||
projects = api.state[PROJECTS]
|
||||
|
||||
# Grab all labels
|
||||
labels = api.state[LABELS]
|
||||
|
||||
# Add all Todoist-defined projects.
|
||||
project_devices = []
|
||||
for project in projects:
|
||||
# Project is an object, not a dict!
|
||||
# Because of that, we convert what we need to a dict.
|
||||
project_data = {
|
||||
CONF_NAME: project[NAME],
|
||||
CONF_ID: project[ID]
|
||||
}
|
||||
project_devices.append(
|
||||
TodoistProjectDevice(hass, project_data, labels, api)
|
||||
)
|
||||
# Cache the names so we can easily look up name->ID.
|
||||
project_id_lookup[project[NAME].lower()] = project[ID]
|
||||
|
||||
# Cache all label names
|
||||
for label in labels:
|
||||
label_id_lookup[label[NAME].lower()] = label[ID]
|
||||
|
||||
# Check config for more projects.
|
||||
extra_projects = config.get(CONF_EXTRA_PROJECTS)
|
||||
for project in extra_projects:
|
||||
# Special filter: By date
|
||||
project_due_date = project.get(CONF_PROJECT_DUE_DATE)
|
||||
|
||||
# Special filter: By label
|
||||
project_label_filter = project.get(CONF_PROJECT_LABEL_WHITELIST)
|
||||
|
||||
# Special filter: By name
|
||||
# Names must be converted into IDs.
|
||||
project_name_filter = project.get(CONF_PROJECT_WHITELIST)
|
||||
project_id_filter = [
|
||||
project_id_lookup[project_name.lower()]
|
||||
for project_name in project_name_filter]
|
||||
|
||||
# Create the custom project and add it to the devices array.
|
||||
project_devices.append(
|
||||
TodoistProjectDevice(
|
||||
hass, project, labels, api, project_due_date,
|
||||
project_label_filter, project_id_filter
|
||||
)
|
||||
)
|
||||
|
||||
add_devices(project_devices)
|
||||
|
||||
# Services:
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def handle_new_task(call):
|
||||
"""Called when a user creates a new Todoist Task from HASS."""
|
||||
project_name = call.data[PROJECT_NAME]
|
||||
project_id = project_id_lookup[project_name]
|
||||
|
||||
# Create the task
|
||||
item = api.items.add(call.data[CONTENT], project_id)
|
||||
|
||||
if LABELS in call.data:
|
||||
task_labels = call.data[LABELS]
|
||||
label_ids = [
|
||||
label_id_lookup[label.lower()]
|
||||
for label in task_labels]
|
||||
item.update(labels=label_ids)
|
||||
|
||||
if PRIORITY in call.data:
|
||||
item.update(priority=call.data[PRIORITY])
|
||||
|
||||
if DUE_DATE in call.data:
|
||||
due_date = dt.parse_datetime(call.data[DUE_DATE])
|
||||
if due_date is None:
|
||||
due = dt.parse_date(call.data[DUE_DATE])
|
||||
due_date = datetime(due.year, due.month, due.day)
|
||||
# Format it in the manner Todoist expects
|
||||
due_date = dt.as_utc(due_date)
|
||||
date_format = '%Y-%m-%dT%H:%M'
|
||||
due_date = datetime.strftime(due_date, date_format)
|
||||
item.update(due_date_utc=due_date)
|
||||
# Commit changes
|
||||
api.commit()
|
||||
_LOGGER.debug("Created Todoist task: %s", call.data[CONTENT])
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_NEW_TASK, handle_new_task,
|
||||
descriptions[DOMAIN][SERVICE_NEW_TASK],
|
||||
schema=NEW_TASK_SERVICE_SCHEMA)
|
||||
|
||||
|
||||
class TodoistProjectDevice(CalendarEventDevice):
|
||||
"""A device for getting the next Task from a Todoist Project."""
|
||||
|
||||
def __init__(self, hass, data, labels, token,
|
||||
latest_task_due_date=None, whitelisted_labels=None,
|
||||
whitelisted_projects=None):
|
||||
"""Create the Todoist Calendar Event Device."""
|
||||
self.data = TodoistProjectData(
|
||||
data, labels, token, latest_task_due_date,
|
||||
whitelisted_labels, whitelisted_projects
|
||||
)
|
||||
|
||||
# Set up the calendar side of things
|
||||
calendar_format = {
|
||||
CONF_NAME: data[CONF_NAME],
|
||||
# Set Entity ID to use the name so we can identify calendars
|
||||
CONF_DEVICE_ID: data[CONF_NAME]
|
||||
}
|
||||
|
||||
super().__init__(hass, calendar_format)
|
||||
|
||||
def update(self):
|
||||
"""Update all Todoist Calendars."""
|
||||
# Set basic calendar data
|
||||
super().update()
|
||||
|
||||
# Set Todoist-specific data that can't easily be grabbed
|
||||
self._cal_data[ALL_TASKS] = [
|
||||
task[SUMMARY] for task in self.data.all_project_tasks]
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up all calendar data."""
|
||||
super().cleanup()
|
||||
self._cal_data[ALL_TASKS] = []
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
if self.data.event is None:
|
||||
# No tasks, we don't REALLY need to show anything.
|
||||
return {}
|
||||
|
||||
attributes = super().device_state_attributes
|
||||
|
||||
# Add additional attributes.
|
||||
attributes[DUE_TODAY] = self.data.event[DUE_TODAY]
|
||||
attributes[OVERDUE] = self.data.event[OVERDUE]
|
||||
attributes[ALL_TASKS] = self._cal_data[ALL_TASKS]
|
||||
attributes[PRIORITY] = self.data.event[PRIORITY]
|
||||
attributes[LABELS] = self.data.event[LABELS]
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
class TodoistProjectData(object):
|
||||
"""
|
||||
Class used by the Task Device service object to hold all Todoist Tasks.
|
||||
|
||||
This is analagous to the GoogleCalendarData found in the Google Calendar
|
||||
component.
|
||||
|
||||
Takes an object with a 'name' field and optionally an 'id' field (either
|
||||
user-defined or from the Todoist API), a Todoist API token, and an optional
|
||||
integer specifying the latest number of days from now a task can be due (7
|
||||
means everything due in the next week, 0 means today, etc.).
|
||||
|
||||
This object has an exposed 'event' property (used by the Calendar platform
|
||||
to determine the next calendar event) and an exposed 'update' method (used
|
||||
by the Calendar platform to poll for new calendar events).
|
||||
|
||||
The 'event' is a representation of a Todoist Task, with defined parameters
|
||||
of 'due_today' (is the task due today?), 'all_day' (does the task have a
|
||||
due date?), 'task_labels' (all labels assigned to the task), 'message'
|
||||
(the content of the task, e.g. 'Fetch Mail'), 'description' (a URL pointing
|
||||
to the task on the Todoist website), 'end_time' (what time the event is
|
||||
due), 'start_time' (what time this event was last updated), 'overdue' (is
|
||||
the task past its due date?), 'priority' (1-4, how important the task is,
|
||||
with 4 being the most important), and 'all_tasks' (all tasks in this
|
||||
project, sorted by how important they are).
|
||||
|
||||
'offset_reached', 'location', and 'friendly_name' are defined by the
|
||||
platform itself, but are not used by this component at all.
|
||||
|
||||
The 'update' method polls the Todoist API for new projects/tasks, as well
|
||||
as any updates to current projects/tasks. This is throttled to every
|
||||
MIN_TIME_BETWEEN_UPDATES minutes.
|
||||
"""
|
||||
|
||||
def __init__(self, project_data, labels, api,
|
||||
latest_task_due_date=None, whitelisted_labels=None,
|
||||
whitelisted_projects=None):
|
||||
"""Initialize a Todoist Project."""
|
||||
self.event = None
|
||||
|
||||
self._api = api
|
||||
self._name = project_data.get(CONF_NAME)
|
||||
# If no ID is defined, fetch all tasks.
|
||||
self._id = project_data.get(CONF_ID)
|
||||
|
||||
# All labels the user has defined, for easy lookup.
|
||||
self._labels = labels
|
||||
# Not tracked: order, indent, comment_count.
|
||||
|
||||
self.all_project_tasks = []
|
||||
|
||||
# The latest date a task can be due (for making lists of everything
|
||||
# due today, or everything due in the next week, for example).
|
||||
if latest_task_due_date is not None:
|
||||
self._latest_due_date = dt.utcnow() + timedelta(
|
||||
days=latest_task_due_date)
|
||||
else:
|
||||
self._latest_due_date = None
|
||||
|
||||
# Only tasks with one of these labels will be included.
|
||||
if whitelisted_labels is not None:
|
||||
self._label_whitelist = whitelisted_labels
|
||||
else:
|
||||
self._label_whitelist = []
|
||||
|
||||
# This project includes only projects with these names.
|
||||
if whitelisted_projects is not None:
|
||||
self._project_id_whitelist = whitelisted_projects
|
||||
else:
|
||||
self._project_id_whitelist = []
|
||||
|
||||
def create_todoist_task(self, data):
|
||||
"""
|
||||
Create a dictionary based on a Task passed from the Todoist API.
|
||||
|
||||
Will return 'None' if the task is to be filtered out.
|
||||
"""
|
||||
task = {}
|
||||
# Fields are required to be in all returned task objects.
|
||||
task[SUMMARY] = data[CONTENT]
|
||||
task[COMPLETED] = data[CHECKED] == 1
|
||||
task[PRIORITY] = data[PRIORITY]
|
||||
task[DESCRIPTION] = 'https://todoist.com/showTask?id={}'.format(
|
||||
data[ID])
|
||||
|
||||
# All task Labels (optional parameter).
|
||||
task[LABELS] = [
|
||||
label[NAME].lower() for label in self._labels
|
||||
if label[ID] in data[LABELS]]
|
||||
|
||||
if self._label_whitelist and (
|
||||
not any(label in task[LABELS]
|
||||
for label in self._label_whitelist)):
|
||||
# We're not on the whitelist, return invalid task.
|
||||
return None
|
||||
|
||||
# Due dates (optional parameter).
|
||||
# The due date is the END date -- the task cannot be completed
|
||||
# past this time.
|
||||
# That means that the START date is the earliest time one can
|
||||
# complete the task.
|
||||
# Generally speaking, that means right now.
|
||||
task[START] = dt.utcnow()
|
||||
if data[DUE_DATE_UTC] is not None:
|
||||
due_date = data[DUE_DATE_UTC]
|
||||
|
||||
# Due dates are represented in RFC3339 format, in UTC.
|
||||
# Home Assistant exclusively uses UTC, so it'll
|
||||
# handle the conversion.
|
||||
time_format = '%a %d %b %Y %H:%M:%S %z'
|
||||
# HASS' built-in parse time function doesn't like
|
||||
# Todoist's time format; strptime has to be used.
|
||||
task[END] = datetime.strptime(due_date, time_format)
|
||||
|
||||
if self._latest_due_date is not None and (
|
||||
task[END] > self._latest_due_date):
|
||||
# This task is out of range of our due date;
|
||||
# it shouldn't be counted.
|
||||
return None
|
||||
|
||||
task[DUE_TODAY] = task[END].date() == datetime.today().date()
|
||||
|
||||
# Special case: Task is overdue.
|
||||
if task[END] <= task[START]:
|
||||
task[OVERDUE] = True
|
||||
# Set end time to the current time plus 1 hour.
|
||||
# We're pretty much guaranteed to update within that 1 hour,
|
||||
# so it should be fine.
|
||||
task[END] = task[START] + timedelta(hours=1)
|
||||
else:
|
||||
task[OVERDUE] = False
|
||||
else:
|
||||
# If we ask for everything due before a certain date, don't count
|
||||
# things which have no due dates.
|
||||
if self._latest_due_date is not None:
|
||||
return None
|
||||
|
||||
# Define values for tasks without due dates
|
||||
task[END] = None
|
||||
task[ALL_DAY] = True
|
||||
task[DUE_TODAY] = False
|
||||
task[OVERDUE] = False
|
||||
|
||||
# Not tracked: id, comments, project_id order, indent, recurring.
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def select_best_task(project_tasks):
|
||||
"""
|
||||
Search through a list of events for the "best" event to select.
|
||||
|
||||
The "best" event is determined by the following criteria:
|
||||
* A proposed event must not be completed
|
||||
* A proposed event must have a end date (otherwise we go with
|
||||
the event at index 0, selected above)
|
||||
* A proposed event must be on the same day or earlier as our
|
||||
current event
|
||||
* If a proposed event is an earlier day than what we have so
|
||||
far, select it
|
||||
* If a proposed event is on the same day as our current event
|
||||
and the proposed event has a higher priority than our current
|
||||
event, select it
|
||||
* If a proposed event is on the same day as our current event,
|
||||
has the same priority as our current event, but is due earlier
|
||||
in the day, select it
|
||||
"""
|
||||
# Start at the end of the list, so if tasks don't have a due date
|
||||
# the newest ones are the most important.
|
||||
|
||||
event = project_tasks[-1]
|
||||
|
||||
for proposed_event in project_tasks:
|
||||
if event == proposed_event:
|
||||
continue
|
||||
if proposed_event[COMPLETED]:
|
||||
# Event is complete!
|
||||
continue
|
||||
if proposed_event[END] is None:
|
||||
# No end time:
|
||||
if event[END] is None and (
|
||||
proposed_event[PRIORITY] < event[PRIORITY]):
|
||||
# They also have no end time,
|
||||
# but we have a higher priority.
|
||||
event = proposed_event
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
elif event[END] is None:
|
||||
# We have an end time, they do not.
|
||||
event = proposed_event
|
||||
continue
|
||||
if proposed_event[END].date() > event[END].date():
|
||||
# Event is too late.
|
||||
continue
|
||||
elif proposed_event[END].date() < event[END].date():
|
||||
# Event is earlier than current, select it.
|
||||
event = proposed_event
|
||||
continue
|
||||
else:
|
||||
if proposed_event[PRIORITY] > event[PRIORITY]:
|
||||
# Proposed event has a higher priority.
|
||||
event = proposed_event
|
||||
continue
|
||||
elif proposed_event[PRIORITY] == event[PRIORITY] and (
|
||||
proposed_event[END] < event[END]):
|
||||
event = proposed_event
|
||||
continue
|
||||
return event
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
if self._id is None:
|
||||
project_task_data = [
|
||||
task for task in self._api.state[TASKS]
|
||||
if not self._project_id_whitelist or
|
||||
task[PROJECT_ID] in self._project_id_whitelist]
|
||||
else:
|
||||
project_task_data = self._api.projects.get_data(self._id)[TASKS]
|
||||
|
||||
# If we have no data, we can just return right away.
|
||||
if not project_task_data:
|
||||
self.event = None
|
||||
return True
|
||||
|
||||
# Keep an updated list of all tasks in this project.
|
||||
project_tasks = []
|
||||
|
||||
for task in project_task_data:
|
||||
todoist_task = self.create_todoist_task(task)
|
||||
if todoist_task is not None:
|
||||
# A None task means it is invalid for this project
|
||||
project_tasks.append(todoist_task)
|
||||
|
||||
if not project_tasks:
|
||||
# We had no valid tasks
|
||||
return True
|
||||
|
||||
# Organize the best tasks (so users can see all the tasks
|
||||
# they have, organized)
|
||||
while len(project_tasks) > 0:
|
||||
best_task = self.select_best_task(project_tasks)
|
||||
_LOGGER.debug("Found Todoist Task: %s", best_task[SUMMARY])
|
||||
project_tasks.remove(best_task)
|
||||
self.all_project_tasks.append(best_task)
|
||||
|
||||
self.event = self.all_project_tasks[0]
|
||||
|
||||
# Convert datetime to a string again
|
||||
if self.event is not None:
|
||||
if self.event[START] is not None:
|
||||
self.event[START] = {
|
||||
DATETIME: self.event[START].strftime(DATE_STR_FORMAT)
|
||||
}
|
||||
if self.event[END] is not None:
|
||||
self.event[END] = {
|
||||
DATETIME: self.event[END].strftime(DATE_STR_FORMAT)
|
||||
}
|
||||
else:
|
||||
# HASS gets cranky if a calendar event never ends
|
||||
# Let's set our "due date" to tomorrow
|
||||
self.event[END] = {
|
||||
DATETIME: (
|
||||
datetime.utcnow() +
|
||||
timedelta(days=1)
|
||||
).strftime(DATE_STR_FORMAT)
|
||||
}
|
||||
_LOGGER.debug("Updated %s", self._name)
|
||||
return True
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
This component provides HA camera support for Abode Security System.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.abode/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
import requests
|
||||
|
||||
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discoveryy_info=None):
|
||||
"""Set up Abode camera devices."""
|
||||
import abodepy.helpers.constants as CONST
|
||||
import abodepy.helpers.timeline as TIMELINE
|
||||
|
||||
data = hass.data[ABODE_DOMAIN]
|
||||
|
||||
devices = []
|
||||
for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA):
|
||||
if data.is_excluded(device):
|
||||
continue
|
||||
|
||||
devices.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE))
|
||||
|
||||
data.devices.extend(devices)
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class AbodeCamera(AbodeDevice, Camera):
|
||||
"""Representation of an Abode camera."""
|
||||
|
||||
def __init__(self, data, device, event):
|
||||
"""Initialize the Abode device."""
|
||||
AbodeDevice.__init__(self, data, device)
|
||||
Camera.__init__(self)
|
||||
self._event = event
|
||||
self._response = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe Abode events."""
|
||||
yield from super().async_added_to_hass()
|
||||
|
||||
self.hass.async_add_job(
|
||||
self._data.abode.events.add_timeline_callback,
|
||||
self._event, self._capture_callback
|
||||
)
|
||||
|
||||
def capture(self):
|
||||
"""Request a new image capture."""
|
||||
return self._device.capture()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def refresh_image(self):
|
||||
"""Find a new image on the timeline."""
|
||||
if self._device.refresh_image():
|
||||
self.get_image()
|
||||
|
||||
def get_image(self):
|
||||
"""Attempt to download the most recent capture."""
|
||||
if self._device.image_url:
|
||||
try:
|
||||
self._response = requests.get(
|
||||
self._device.image_url, stream=True)
|
||||
|
||||
self._response.raise_for_status()
|
||||
except requests.HTTPError as err:
|
||||
_LOGGER.warning("Failed to get camera image: %s", err)
|
||||
self._response = None
|
||||
else:
|
||||
self._response = None
|
||||
|
||||
def camera_image(self):
|
||||
"""Get a camera image."""
|
||||
self.refresh_image()
|
||||
|
||||
if self._response:
|
||||
return self._response.content
|
||||
|
||||
return None
|
||||
|
||||
def _capture_callback(self, capture):
|
||||
"""Update the image with the device then refresh device."""
|
||||
self._device.update_image_location(capture)
|
||||
self.get_image()
|
||||
self.schedule_update_ha_state()
|
||||
@@ -7,7 +7,7 @@ https://home-assistant.io/components/camera.axis/
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT,
|
||||
CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
|
||||
@@ -19,38 +19,44 @@ DOMAIN = 'axis'
|
||||
DEPENDENCIES = [DOMAIN]
|
||||
|
||||
|
||||
def _get_image_url(host, mode):
|
||||
def _get_image_url(host, port, mode):
|
||||
if mode == 'mjpeg':
|
||||
return 'http://{}/axis-cgi/mjpg/video.cgi'.format(host)
|
||||
return 'http://{}:{}/axis-cgi/mjpg/video.cgi'.format(host, port)
|
||||
elif mode == 'single':
|
||||
return 'http://{}/axis-cgi/jpg/image.cgi'.format(host)
|
||||
return 'http://{}:{}/axis-cgi/jpg/image.cgi'.format(host, port)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Axis camera."""
|
||||
config = {
|
||||
camera_config = {
|
||||
CONF_NAME: discovery_info[CONF_NAME],
|
||||
CONF_USERNAME: discovery_info[CONF_USERNAME],
|
||||
CONF_PASSWORD: discovery_info[CONF_PASSWORD],
|
||||
CONF_MJPEG_URL: _get_image_url(discovery_info[CONF_HOST], 'mjpeg'),
|
||||
CONF_MJPEG_URL: _get_image_url(discovery_info[CONF_HOST],
|
||||
str(discovery_info[CONF_PORT]),
|
||||
'mjpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info[CONF_HOST],
|
||||
str(discovery_info[CONF_PORT]),
|
||||
'single'),
|
||||
CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION,
|
||||
}
|
||||
add_devices([AxisCamera(hass, config)])
|
||||
add_devices([AxisCamera(hass,
|
||||
camera_config,
|
||||
str(discovery_info[CONF_PORT]))])
|
||||
|
||||
|
||||
class AxisCamera(MjpegCamera):
|
||||
"""AxisCamera class."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
def __init__(self, hass, config, port):
|
||||
"""Initialize Axis Communications camera component."""
|
||||
super().__init__(hass, config)
|
||||
self.port = port
|
||||
async_dispatcher_connect(hass,
|
||||
DOMAIN + '_' + config[CONF_NAME] + '_new_ip',
|
||||
self._new_ip)
|
||||
|
||||
def _new_ip(self, host):
|
||||
"""Set new IP for video stream."""
|
||||
self._mjpeg_url = _get_image_url(host, 'mjpeg')
|
||||
self._still_image_url = _get_image_url(host, 'mjpeg')
|
||||
self._mjpeg_url = _get_image_url(host, self.port, 'mjpeg')
|
||||
self._still_image_url = _get_image_url(host, self.port, 'single')
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Support for viewing the camera feed from a DoorBird video doorbell."""
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
||||
from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
DEPENDENCIES = ['doorbird']
|
||||
|
||||
_CAMERA_LIVE = "DoorBird Live"
|
||||
_CAMERA_LAST_VISITOR = "DoorBird Last Ring"
|
||||
_LIVE_INTERVAL = datetime.timedelta(seconds=1)
|
||||
_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_TIMEOUT = 10 # seconds
|
||||
|
||||
CONF_SHOW_LAST_VISITOR = 'last_visitor'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SHOW_LAST_VISITOR, default=False): cv.boolean
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the DoorBird camera platform."""
|
||||
device = hass.data.get(DOORBIRD_DOMAIN)
|
||||
|
||||
_LOGGER.debug("Adding DoorBird camera %s", _CAMERA_LIVE)
|
||||
entities = [DoorBirdCamera(device.live_image_url, _CAMERA_LIVE,
|
||||
_LIVE_INTERVAL)]
|
||||
|
||||
if config.get(CONF_SHOW_LAST_VISITOR):
|
||||
_LOGGER.debug("Adding DoorBird camera %s", _CAMERA_LAST_VISITOR)
|
||||
entities.append(DoorBirdCamera(device.history_image_url(1),
|
||||
_CAMERA_LAST_VISITOR,
|
||||
_LAST_VISITOR_INTERVAL))
|
||||
|
||||
async_add_devices(entities)
|
||||
_LOGGER.info("Added DoorBird camera(s)")
|
||||
|
||||
|
||||
class DoorBirdCamera(Camera):
|
||||
"""The camera on a DoorBird device."""
|
||||
|
||||
def __init__(self, url, name, interval=None):
|
||||
"""Initialize the camera on a DoorBird device."""
|
||||
self._url = url
|
||||
self._name = name
|
||||
self._last_image = None
|
||||
self._interval = interval or datetime.timedelta
|
||||
self._last_update = datetime.datetime.min
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Get the name of the camera."""
|
||||
return self._name
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Pull a still image from the camera."""
|
||||
now = datetime.datetime.now()
|
||||
|
||||
if self._last_image and now - self._last_update < self._interval:
|
||||
return self._last_image
|
||||
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
|
||||
with async_timeout.timeout(_TIMEOUT, loop=self.hass.loop):
|
||||
response = yield from websession.get(self._url)
|
||||
|
||||
self._last_image = yield from response.read()
|
||||
self._last_update = now
|
||||
return self._last_image
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Camera image timed out")
|
||||
return self._last_image
|
||||
except aiohttp.ClientError as error:
|
||||
_LOGGER.error("Error getting camera image: %s", error)
|
||||
return self._last_image
|
||||
@@ -15,7 +15,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pyfoscam==1.2']
|
||||
REQUIREMENTS = ['libpyfoscam==1.0']
|
||||
|
||||
CONF_IP = 'ip'
|
||||
|
||||
@@ -53,7 +53,7 @@ class FoscamCam(Camera):
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
self._motion_status = False
|
||||
|
||||
from foscam import FoscamCamera
|
||||
from libpyfoscam import FoscamCamera
|
||||
|
||||
self._foscam_session = FoscamCamera(ip_address, port, self._username,
|
||||
self._password, verbose=False)
|
||||
|
||||
@@ -77,7 +77,7 @@ class USPSCamera(Camera):
|
||||
def model(self):
|
||||
"""Return date of mail as model."""
|
||||
try:
|
||||
return 'Date: {}'.format(self._usps.mail[0]['date'])
|
||||
return 'Date: {}'.format(str(self._usps.mail[0]['date']))
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.const import CONF_PORT
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['uvcclient==0.10.0']
|
||||
REQUIREMENTS = ['uvcclient==0.10.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ class GenericThermostat(ClimateDevice):
|
||||
"""Handle heater switch state changes."""
|
||||
if new_state is None:
|
||||
return
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_keep_alive(self, time):
|
||||
|
||||
@@ -47,8 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
devices = []
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMThermostat(hass, conf)
|
||||
new_device.link_homematic()
|
||||
new_device = HMThermostat(conf)
|
||||
devices.append(new_device)
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
@@ -196,6 +196,11 @@ class RoundThermostat(ClimateDevice):
|
||||
if val['id'] == self._id:
|
||||
data = val
|
||||
|
||||
except KeyError:
|
||||
_LOGGER.error("Update failed from Honeywell server")
|
||||
self.client.user_data = None
|
||||
return
|
||||
|
||||
except StopIteration:
|
||||
_LOGGER.error("Did not receive any temperature data from the "
|
||||
"evohomeclient API")
|
||||
|
||||
@@ -1,68 +1,136 @@
|
||||
"""
|
||||
Support for KNX thermostats.
|
||||
Support for KNX/IP climate devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.knx/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice)
|
||||
from homeassistant.const import (CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE)
|
||||
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
|
||||
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice
|
||||
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ADDRESS = 'address'
|
||||
CONF_SETPOINT_ADDRESS = 'setpoint_address'
|
||||
CONF_TEMPERATURE_ADDRESS = 'temperature_address'
|
||||
CONF_TARGET_TEMPERATURE_ADDRESS = 'target_temperature_address'
|
||||
CONF_OPERATION_MODE_ADDRESS = 'operation_mode_address'
|
||||
CONF_OPERATION_MODE_STATE_ADDRESS = 'operation_mode_state_address'
|
||||
CONF_CONTROLLER_STATUS_ADDRESS = 'controller_status_address'
|
||||
CONF_CONTROLLER_STATUS_STATE_ADDRESS = 'controller_status_state_address'
|
||||
CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = \
|
||||
'operation_mode_frost_protection_address'
|
||||
CONF_OPERATION_MODE_NIGHT_ADDRESS = 'operation_mode_night_address'
|
||||
CONF_OPERATION_MODE_COMFORT_ADDRESS = 'operation_mode_comfort_address'
|
||||
|
||||
DEFAULT_NAME = 'KNX Thermostat'
|
||||
DEFAULT_NAME = 'KNX Climate'
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_SETPOINT_ADDRESS): cv.string,
|
||||
vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_OPERATION_MODE_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_STATE_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_COMFORT_ADDRESS): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Create and add an entity based on the configuration."""
|
||||
add_devices([KNXThermostat(hass, KNXConfig(config))])
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up climate(s) for KNX platform."""
|
||||
if DATA_KNX not in hass.data \
|
||||
or not hass.data[DATA_KNX].initialized:
|
||||
return False
|
||||
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, async_add_devices)
|
||||
else:
|
||||
async_add_devices_config(hass, config, async_add_devices)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
"""Representation of a KNX thermostat.
|
||||
@callback
|
||||
def async_add_devices_discovery(hass, discovery_info, async_add_devices):
|
||||
"""Set up climates for KNX platform configured within plattform."""
|
||||
entities = []
|
||||
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
device = hass.data[DATA_KNX].xknx.devices[device_name]
|
||||
entities.append(KNXClimate(hass, device))
|
||||
async_add_devices(entities)
|
||||
|
||||
A KNX thermostat will has the following parameters:
|
||||
- temperature (current temperature)
|
||||
- setpoint (target temperature in HASS terms)
|
||||
- operation mode selection (comfort/night/frost protection)
|
||||
|
||||
This version supports only polling. Messages from the KNX bus do not
|
||||
automatically update the state of the thermostat (to be implemented
|
||||
in future releases)
|
||||
"""
|
||||
@callback
|
||||
def async_add_devices_config(hass, config, async_add_devices):
|
||||
"""Set up climate for KNX platform configured within plattform."""
|
||||
import xknx
|
||||
climate = xknx.devices.Climate(
|
||||
hass.data[DATA_KNX].xknx,
|
||||
name=config.get(CONF_NAME),
|
||||
group_address_temperature=config.get(
|
||||
CONF_TEMPERATURE_ADDRESS),
|
||||
group_address_target_temperature=config.get(
|
||||
CONF_TARGET_TEMPERATURE_ADDRESS),
|
||||
group_address_setpoint=config.get(
|
||||
CONF_SETPOINT_ADDRESS),
|
||||
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_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)
|
||||
async_add_devices([KNXClimate(hass, climate)])
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize the thermostat based on the given configuration."""
|
||||
KNXMultiAddressDevice.__init__(
|
||||
self, hass, config, ['temperature', 'setpoint'], ['mode'])
|
||||
|
||||
self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius
|
||||
class KNXClimate(ClimateDevice):
|
||||
"""Representation of a KNX climate."""
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialization of KNXClimate."""
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
|
||||
self._unit_of_measurement = TEMP_CELSIUS
|
||||
self._away = False # not yet supported
|
||||
self._is_fan_on = False # not yet supported
|
||||
self._current_temp = None
|
||||
self._target_temp = None
|
||||
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
def after_update_callback(device):
|
||||
"""Callback after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
yield from self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state, is needed for the KNX thermostat."""
|
||||
return True
|
||||
"""No polling needed within KNX."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
@@ -72,32 +140,42 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temp
|
||||
return self.device.temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
if self.device.supports_target_temperature:
|
||||
return self.device.target_temperature
|
||||
return None
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
from knxip.conversion import float_to_knx2
|
||||
if self.device.supports_target_temperature:
|
||||
yield from self.device.set_target_temperature(temperature)
|
||||
|
||||
self.set_value('setpoint', float_to_knx2(temperature))
|
||||
_LOGGER.debug("Set target temperature to %s", temperature)
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self.device.supports_operation_mode:
|
||||
return self.device.operation_mode.value
|
||||
return None
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return [operation_mode.value for
|
||||
operation_mode in
|
||||
self.device.get_supported_operation_modes()]
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def update(self):
|
||||
"""Update KNX climate."""
|
||||
from knxip.conversion import knx2_to_float
|
||||
|
||||
super().update()
|
||||
|
||||
self._current_temp = knx2_to_float(self.value('temperature'))
|
||||
self._target_temp = knx2_to_float(self.value('setpoint'))
|
||||
if self.device.supports_operation_mode:
|
||||
from xknx.knx import HVACOperationMode
|
||||
knx_operation_mode = HVACOperationMode(operation_mode)
|
||||
yield from self.device.set_operation_mode(knx_operation_mode)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
Support for Tesla HVAC system.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/climate.tesla/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT
|
||||
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice
|
||||
from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT, TEMP_CELSIUS, ATTR_TEMPERATURE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['tesla']
|
||||
|
||||
OPERATION_LIST = [STATE_ON, STATE_OFF]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Tesla climate platform."""
|
||||
devices = [TeslaThermostat(device, hass.data[TESLA_DOMAIN]['controller'])
|
||||
for device in hass.data[TESLA_DOMAIN]['devices']['climate']]
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class TeslaThermostat(TeslaDevice, ClimateDevice):
|
||||
"""Representation of a Tesla climate."""
|
||||
|
||||
def __init__(self, tesla_device, controller):
|
||||
"""Initialize the Tesla device."""
|
||||
super().__init__(tesla_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
|
||||
self._target_temperature = None
|
||||
self._temperature = None
|
||||
self._name = self.tesla_device.name
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. On or Off."""
|
||||
mode = self.tesla_device.is_hvac_enabled()
|
||||
if mode:
|
||||
return OPERATION_LIST[0] # On
|
||||
else:
|
||||
return OPERATION_LIST[1] # Off
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return OPERATION_LIST
|
||||
|
||||
def update(self):
|
||||
"""Called by the Tesla device callback to update state."""
|
||||
_LOGGER.debug("Updating: %s", self._name)
|
||||
self.tesla_device.update()
|
||||
self._target_temperature = self.tesla_device.get_goal_temp()
|
||||
self._temperature = self.tesla_device.get_current_temp()
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
tesla_temp_units = self.tesla_device.measurement
|
||||
|
||||
if tesla_temp_units == 'F':
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperatures."""
|
||||
_LOGGER.debug("Setting temperature for: %s", self._name)
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature:
|
||||
self.tesla_device.set_temperature(temperature)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set HVAC mode (auto, cool, heat, off)."""
|
||||
_LOGGER.debug("Setting mode for: %s", self._name)
|
||||
if operation_mode == OPERATION_LIST[1]: # off
|
||||
self.tesla_device.set_status(False)
|
||||
elif operation_mode == OPERATION_LIST[0]: # heat
|
||||
self.tesla_device.set_status(True)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Component to integrate the Home Assistant cloud."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from . import http_api, auth_api
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
REQUIREMENTS = ['warrant==0.2.0']
|
||||
DEPENDENCIES = ['http']
|
||||
CONF_MODE = 'mode'
|
||||
MODE_DEV = 'development'
|
||||
MODE_STAGING = 'staging'
|
||||
MODE_PRODUCTION = 'production'
|
||||
DEFAULT_MODE = MODE_DEV
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
|
||||
vol.In([MODE_DEV, MODE_STAGING, MODE_PRODUCTION]),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Initialize the Home Assistant cloud."""
|
||||
mode = MODE_PRODUCTION
|
||||
|
||||
if DOMAIN in config:
|
||||
mode = config[DOMAIN].get(CONF_MODE)
|
||||
|
||||
if mode != 'development':
|
||||
_LOGGER.error('Only development mode is currently allowed.')
|
||||
return False
|
||||
|
||||
data = hass.data[DOMAIN] = {
|
||||
'mode': mode
|
||||
}
|
||||
|
||||
data['auth'] = yield from hass.async_add_job(auth_api.load_auth, hass)
|
||||
|
||||
yield from http_api.async_setup(hass)
|
||||
return True
|
||||
@@ -0,0 +1,270 @@
|
||||
"""Package to offer tools to authenticate with the cloud."""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .const import AUTH_FILE, SERVERS
|
||||
from .util import get_mode
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CloudError(Exception):
|
||||
"""Base class for cloud related errors."""
|
||||
|
||||
|
||||
class Unauthenticated(CloudError):
|
||||
"""Raised when authentication failed."""
|
||||
|
||||
|
||||
class UserNotFound(CloudError):
|
||||
"""Raised when a user is not found."""
|
||||
|
||||
|
||||
class UserNotConfirmed(CloudError):
|
||||
"""Raised when a user has not confirmed email yet."""
|
||||
|
||||
|
||||
class ExpiredCode(CloudError):
|
||||
"""Raised when an expired code is encoutered."""
|
||||
|
||||
|
||||
class InvalidCode(CloudError):
|
||||
"""Raised when an invalid code is submitted."""
|
||||
|
||||
|
||||
class PasswordChangeRequired(CloudError):
|
||||
"""Raised when a password change is required."""
|
||||
|
||||
def __init__(self, message='Password change required.'):
|
||||
"""Initialize a password change required error."""
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class UnknownError(CloudError):
|
||||
"""Raised when an unknown error occurrs."""
|
||||
|
||||
|
||||
AWS_EXCEPTIONS = {
|
||||
'UserNotFoundException': UserNotFound,
|
||||
'NotAuthorizedException': Unauthenticated,
|
||||
'ExpiredCodeException': ExpiredCode,
|
||||
'UserNotConfirmedException': UserNotConfirmed,
|
||||
'PasswordResetRequiredException': PasswordChangeRequired,
|
||||
'CodeMismatchException': InvalidCode,
|
||||
}
|
||||
|
||||
|
||||
def _map_aws_exception(err):
|
||||
"""Map AWS exception to our exceptions."""
|
||||
ex = AWS_EXCEPTIONS.get(err.response['Error']['Code'], UnknownError)
|
||||
return ex(err.response['Error']['Message'])
|
||||
|
||||
|
||||
def load_auth(hass):
|
||||
"""Load authentication from disk and verify it."""
|
||||
info = _read_info(hass)
|
||||
|
||||
if info is None:
|
||||
return Auth(hass)
|
||||
|
||||
auth = Auth(hass, _cognito(
|
||||
hass,
|
||||
id_token=info['id_token'],
|
||||
access_token=info['access_token'],
|
||||
refresh_token=info['refresh_token'],
|
||||
))
|
||||
|
||||
if auth.validate_auth():
|
||||
return auth
|
||||
|
||||
return Auth(hass)
|
||||
|
||||
|
||||
def register(hass, email, password):
|
||||
"""Register a new account."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(hass, username=email)
|
||||
try:
|
||||
cognito.register(email, password)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def confirm_register(hass, confirmation_code, email):
|
||||
"""Confirm confirmation code after registration."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(hass, username=email)
|
||||
try:
|
||||
cognito.confirm_sign_up(confirmation_code, email)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def forgot_password(hass, email):
|
||||
"""Initiate forgotten password flow."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(hass, username=email)
|
||||
try:
|
||||
cognito.initiate_forgot_password()
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def confirm_forgot_password(hass, confirmation_code, email, new_password):
|
||||
"""Confirm forgotten password code and change password."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(hass, username=email)
|
||||
try:
|
||||
cognito.confirm_forgot_password(confirmation_code, new_password)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
class Auth(object):
|
||||
"""Class that holds Cloud authentication."""
|
||||
|
||||
def __init__(self, hass, cognito=None):
|
||||
"""Initialize Hass cloud info object."""
|
||||
self.hass = hass
|
||||
self.cognito = cognito
|
||||
self.account = None
|
||||
|
||||
@property
|
||||
def is_logged_in(self):
|
||||
"""Return if user is logged in."""
|
||||
return self.account is not None
|
||||
|
||||
def validate_auth(self):
|
||||
"""Validate that the contained auth is valid."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
try:
|
||||
self._refresh_account_info()
|
||||
except ClientError as err:
|
||||
if err.response['Error']['Code'] != 'NotAuthorizedException':
|
||||
_LOGGER.error('Unexpected error verifying auth: %s', err)
|
||||
return False
|
||||
|
||||
try:
|
||||
self.renew_access_token()
|
||||
self._refresh_account_info()
|
||||
except ClientError:
|
||||
_LOGGER.error('Unable to refresh auth token: %s', err)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def login(self, username, password):
|
||||
"""Login using a username and password."""
|
||||
from botocore.exceptions import ClientError
|
||||
from warrant.exceptions import ForceChangePasswordException
|
||||
|
||||
cognito = _cognito(self.hass, username=username)
|
||||
|
||||
try:
|
||||
cognito.authenticate(password=password)
|
||||
self.cognito = cognito
|
||||
self._refresh_account_info()
|
||||
_write_info(self.hass, self)
|
||||
|
||||
except ForceChangePasswordException as err:
|
||||
raise PasswordChangeRequired
|
||||
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
def _refresh_account_info(self):
|
||||
"""Refresh the account info.
|
||||
|
||||
Raises boto3 exceptions.
|
||||
"""
|
||||
self.account = self.cognito.get_user()
|
||||
|
||||
def renew_access_token(self):
|
||||
"""Refresh token."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
try:
|
||||
self.cognito.renew_access_token()
|
||||
_write_info(self.hass, self)
|
||||
return True
|
||||
except ClientError as err:
|
||||
_LOGGER.error('Error refreshing token: %s', err)
|
||||
return False
|
||||
|
||||
def logout(self):
|
||||
"""Invalidate token."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
try:
|
||||
self.cognito.logout()
|
||||
self.account = None
|
||||
_write_info(self.hass, self)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def _read_info(hass):
|
||||
"""Read auth file."""
|
||||
path = hass.config.path(AUTH_FILE)
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
|
||||
with open(path) as file:
|
||||
return json.load(file).get(get_mode(hass))
|
||||
|
||||
|
||||
def _write_info(hass, auth):
|
||||
"""Write auth info for specified mode.
|
||||
|
||||
Pass in None for data to remove authentication for that mode.
|
||||
"""
|
||||
path = hass.config.path(AUTH_FILE)
|
||||
mode = get_mode(hass)
|
||||
|
||||
if os.path.isfile(path):
|
||||
with open(path) as file:
|
||||
content = json.load(file)
|
||||
else:
|
||||
content = {}
|
||||
|
||||
if auth.is_logged_in:
|
||||
content[mode] = {
|
||||
'id_token': auth.cognito.id_token,
|
||||
'access_token': auth.cognito.access_token,
|
||||
'refresh_token': auth.cognito.refresh_token,
|
||||
}
|
||||
else:
|
||||
content.pop(mode, None)
|
||||
|
||||
with open(path, 'wt') as file:
|
||||
file.write(json.dumps(content, indent=4, sort_keys=True))
|
||||
|
||||
|
||||
def _cognito(hass, **kwargs):
|
||||
"""Get the client credentials."""
|
||||
from warrant import Cognito
|
||||
|
||||
mode = get_mode(hass)
|
||||
|
||||
info = SERVERS.get(mode)
|
||||
|
||||
if info is None:
|
||||
raise ValueError('Mode {} is not supported.'.format(mode))
|
||||
|
||||
cognito = Cognito(
|
||||
user_pool_id=info['identity_pool_id'],
|
||||
client_id=info['client_id'],
|
||||
user_pool_region=info['region'],
|
||||
access_key=info['access_key_id'],
|
||||
secret_key=info['secret_access_key'],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
return cognito
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Constants for the cloud component."""
|
||||
DOMAIN = 'cloud'
|
||||
REQUEST_TIMEOUT = 10
|
||||
AUTH_FILE = '.cloud'
|
||||
|
||||
SERVERS = {
|
||||
'development': {
|
||||
'client_id': '3k755iqfcgv8t12o4pl662mnos',
|
||||
'identity_pool_id': 'us-west-2_vDOfweDJo',
|
||||
'region': 'us-west-2',
|
||||
'access_key_id': 'AKIAJGRK7MILPRJTT2ZQ',
|
||||
'secret_access_key': 'lscdYBApxrLWL0HKuVqVXWv3ou8ZVXgG7rZBu/Sz'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
"""The HTTP api to control the cloud integration."""
|
||||
import asyncio
|
||||
from functools import wraps
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components.http import (
|
||||
HomeAssistantView, RequestDataValidator)
|
||||
|
||||
from . import auth_api
|
||||
from .const import REQUEST_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
"""Initialize the HTTP api."""
|
||||
hass.http.register_view(CloudLoginView)
|
||||
hass.http.register_view(CloudLogoutView)
|
||||
hass.http.register_view(CloudAccountView)
|
||||
hass.http.register_view(CloudRegisterView)
|
||||
hass.http.register_view(CloudConfirmRegisterView)
|
||||
hass.http.register_view(CloudForgotPasswordView)
|
||||
hass.http.register_view(CloudConfirmForgotPasswordView)
|
||||
|
||||
|
||||
_CLOUD_ERRORS = {
|
||||
auth_api.UserNotFound: (400, "User does not exist."),
|
||||
auth_api.UserNotConfirmed: (400, 'Email not confirmed.'),
|
||||
auth_api.Unauthenticated: (401, 'Authentication failed.'),
|
||||
auth_api.PasswordChangeRequired: (400, 'Password change required.'),
|
||||
auth_api.ExpiredCode: (400, 'Confirmation code has expired.'),
|
||||
auth_api.InvalidCode: (400, 'Invalid confirmation code.'),
|
||||
asyncio.TimeoutError: (502, 'Unable to reach the Home Assistant cloud.')
|
||||
}
|
||||
|
||||
|
||||
def _handle_cloud_errors(handler):
|
||||
"""Helper method to handle auth errors."""
|
||||
@asyncio.coroutine
|
||||
@wraps(handler)
|
||||
def error_handler(view, request, *args, **kwargs):
|
||||
"""Handle exceptions that raise from the wrapped request handler."""
|
||||
try:
|
||||
result = yield from handler(view, request, *args, **kwargs)
|
||||
return result
|
||||
|
||||
except (auth_api.CloudError, asyncio.TimeoutError) as err:
|
||||
err_info = _CLOUD_ERRORS.get(err.__class__)
|
||||
if err_info is None:
|
||||
err_info = (502, 'Unexpected error: {}'.format(err))
|
||||
status, msg = err_info
|
||||
return view.json_message(msg, status_code=status,
|
||||
message_code=err.__class__.__name__)
|
||||
|
||||
return error_handler
|
||||
|
||||
|
||||
class CloudLoginView(HomeAssistantView):
|
||||
"""Login to Home Assistant cloud."""
|
||||
|
||||
url = '/api/cloud/login'
|
||||
name = 'api:cloud:login'
|
||||
|
||||
@asyncio.coroutine
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('email'): str,
|
||||
vol.Required('password'): str,
|
||||
}))
|
||||
def post(self, request, data):
|
||||
"""Handle login request."""
|
||||
hass = request.app['hass']
|
||||
auth = hass.data['cloud']['auth']
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(auth.login, data['email'],
|
||||
data['password'])
|
||||
|
||||
return self.json(_auth_data(auth))
|
||||
|
||||
|
||||
class CloudLogoutView(HomeAssistantView):
|
||||
"""Log out of the Home Assistant cloud."""
|
||||
|
||||
url = '/api/cloud/logout'
|
||||
name = 'api:cloud:logout'
|
||||
|
||||
@asyncio.coroutine
|
||||
@_handle_cloud_errors
|
||||
def post(self, request):
|
||||
"""Handle logout request."""
|
||||
hass = request.app['hass']
|
||||
auth = hass.data['cloud']['auth']
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(auth.logout)
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
class CloudAccountView(HomeAssistantView):
|
||||
"""View to retrieve account info."""
|
||||
|
||||
url = '/api/cloud/account'
|
||||
name = 'api:cloud:account'
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Get account info."""
|
||||
hass = request.app['hass']
|
||||
auth = hass.data['cloud']['auth']
|
||||
|
||||
if not auth.is_logged_in:
|
||||
return self.json_message('Not logged in', 400)
|
||||
|
||||
return self.json(_auth_data(auth))
|
||||
|
||||
|
||||
class CloudRegisterView(HomeAssistantView):
|
||||
"""Register on the Home Assistant cloud."""
|
||||
|
||||
url = '/api/cloud/register'
|
||||
name = 'api:cloud:register'
|
||||
|
||||
@asyncio.coroutine
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('email'): str,
|
||||
vol.Required('password'): vol.All(str, vol.Length(min=6)),
|
||||
}))
|
||||
def post(self, request, data):
|
||||
"""Handle registration request."""
|
||||
hass = request.app['hass']
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.register, hass, data['email'], data['password'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
class CloudConfirmRegisterView(HomeAssistantView):
|
||||
"""Confirm registration on the Home Assistant cloud."""
|
||||
|
||||
url = '/api/cloud/confirm_register'
|
||||
name = 'api:cloud:confirm_register'
|
||||
|
||||
@asyncio.coroutine
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('confirmation_code'): str,
|
||||
vol.Required('email'): str,
|
||||
}))
|
||||
def post(self, request, data):
|
||||
"""Handle registration confirmation request."""
|
||||
hass = request.app['hass']
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.confirm_register, hass, data['confirmation_code'],
|
||||
data['email'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
class CloudForgotPasswordView(HomeAssistantView):
|
||||
"""View to start Forgot Password flow.."""
|
||||
|
||||
url = '/api/cloud/forgot_password'
|
||||
name = 'api:cloud:forgot_password'
|
||||
|
||||
@asyncio.coroutine
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('email'): str,
|
||||
}))
|
||||
def post(self, request, data):
|
||||
"""Handle forgot password request."""
|
||||
hass = request.app['hass']
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.forgot_password, hass, data['email'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
class CloudConfirmForgotPasswordView(HomeAssistantView):
|
||||
"""View to finish Forgot Password flow.."""
|
||||
|
||||
url = '/api/cloud/confirm_forgot_password'
|
||||
name = 'api:cloud:confirm_forgot_password'
|
||||
|
||||
@asyncio.coroutine
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('confirmation_code'): str,
|
||||
vol.Required('email'): str,
|
||||
vol.Required('new_password'): vol.All(str, vol.Length(min=6))
|
||||
}))
|
||||
def post(self, request, data):
|
||||
"""Handle forgot password confirm request."""
|
||||
hass = request.app['hass']
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.confirm_forgot_password, hass,
|
||||
data['confirmation_code'], data['email'],
|
||||
data['new_password'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
def _auth_data(auth):
|
||||
"""Generate the auth data JSON response."""
|
||||
return {
|
||||
'email': auth.account.email
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Utilities for the cloud integration."""
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def get_mode(hass):
|
||||
"""Return the current mode of the cloud component.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return hass.data[DOMAIN]['mode']
|
||||
@@ -14,7 +14,7 @@ from homeassistant.util.yaml import load_yaml, dump
|
||||
|
||||
DOMAIN = 'config'
|
||||
DEPENDENCIES = ['http']
|
||||
SECTIONS = ('core', 'group', 'hassbian', 'automation', 'script')
|
||||
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script')
|
||||
ON_DEMAND = ('zwave')
|
||||
|
||||
|
||||
@@ -77,11 +77,11 @@ class BaseEditConfigView(HomeAssistantView):
|
||||
"""Empty config if file not found."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _get_value(self, data, config_key):
|
||||
def _get_value(self, hass, data, config_key):
|
||||
"""Get value."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _write_value(self, data, config_key, new_value):
|
||||
def _write_value(self, hass, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -90,7 +90,7 @@ class BaseEditConfigView(HomeAssistantView):
|
||||
"""Fetch device specific config."""
|
||||
hass = request.app['hass']
|
||||
current = yield from self.read_config(hass)
|
||||
value = self._get_value(current, config_key)
|
||||
value = self._get_value(hass, current, config_key)
|
||||
|
||||
if value is None:
|
||||
return self.json_message('Resource not found', 404)
|
||||
@@ -121,7 +121,7 @@ class BaseEditConfigView(HomeAssistantView):
|
||||
path = hass.config.path(self.path)
|
||||
|
||||
current = yield from self.read_config(hass)
|
||||
self._write_value(current, config_key, data)
|
||||
self._write_value(hass, current, config_key, data)
|
||||
|
||||
yield from hass.async_add_job(_write, path, current)
|
||||
|
||||
@@ -149,11 +149,11 @@ class EditKeyBasedConfigView(BaseEditConfigView):
|
||||
"""Return an empty config."""
|
||||
return {}
|
||||
|
||||
def _get_value(self, data, config_key):
|
||||
def _get_value(self, hass, data, config_key):
|
||||
"""Get value."""
|
||||
return data.get(config_key, {})
|
||||
|
||||
def _write_value(self, data, config_key, new_value):
|
||||
def _write_value(self, hass, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
data.setdefault(config_key, {}).update(new_value)
|
||||
|
||||
@@ -165,14 +165,14 @@ class EditIdBasedConfigView(BaseEditConfigView):
|
||||
"""Return an empty config."""
|
||||
return []
|
||||
|
||||
def _get_value(self, data, config_key):
|
||||
def _get_value(self, hass, data, config_key):
|
||||
"""Get value."""
|
||||
return next(
|
||||
(val for val in data if val.get(CONF_ID) == config_key), None)
|
||||
|
||||
def _write_value(self, data, config_key, new_value):
|
||||
def _write_value(self, hass, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
value = self._get_value(data, config_key)
|
||||
value = self._get_value(hass, data, config_key)
|
||||
|
||||
if value is None:
|
||||
value = {CONF_ID: config_key}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Provide configuration end points for Customize."""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.config import EditKeyBasedConfigView
|
||||
from homeassistant.components import async_reload_core_config
|
||||
from homeassistant.config import DATA_CUSTOMIZE
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONFIG_PATH = 'customize.yaml'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
"""Set up the Customize config API."""
|
||||
hass.http.register_view(CustomizeConfigView(
|
||||
'customize', 'config', CONFIG_PATH, cv.entity_id, dict,
|
||||
post_write_hook=async_reload_core_config
|
||||
))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class CustomizeConfigView(EditKeyBasedConfigView):
|
||||
"""Configure a list of entries."""
|
||||
|
||||
def _get_value(self, hass, data, config_key):
|
||||
"""Get value."""
|
||||
customize = hass.data.get(DATA_CUSTOMIZE, {}).get(config_key) or {}
|
||||
return {'global': customize, 'local': data.get(config_key, {})}
|
||||
|
||||
def _write_value(self, hass, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
data[config_key] = new_value
|
||||
|
||||
state = hass.states.get(config_key)
|
||||
state_attributes = dict(state.attributes)
|
||||
state_attributes.update(new_value)
|
||||
hass.states.async_set(config_key, state.state, state_attributes)
|
||||
@@ -55,6 +55,7 @@ class ZWaveNodeValueView(HomeAssistantView):
|
||||
'label': entity_values.primary.label,
|
||||
'index': entity_values.primary.index,
|
||||
'instance': entity_values.primary.instance,
|
||||
'poll_intensity': entity_values.primary.poll_intensity,
|
||||
}
|
||||
return self.json(values_data)
|
||||
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
"""
|
||||
Component to count within automations.
|
||||
|
||||
For more details about this component, please refer to the documentation
|
||||
at https://home-assistant.io/components/counter/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_INITIAL = 'initial'
|
||||
ATTR_STEP = 'step'
|
||||
|
||||
CONF_INITIAL = 'initial'
|
||||
CONF_STEP = 'step'
|
||||
|
||||
DEFAULT_INITIAL = 0
|
||||
DEFAULT_STEP = 1
|
||||
DOMAIN = 'counter'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
SERVICE_DECREMENT = 'decrement'
|
||||
SERVICE_INCREMENT = 'increment'
|
||||
SERVICE_RESET = 'reset'
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: vol.Any({
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int,
|
||||
}, None)
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def increment(hass, entity_id):
|
||||
"""Increment a counter."""
|
||||
hass.add_job(async_increment, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_increment(hass, entity_id):
|
||||
"""Increment a counter."""
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
DOMAIN, SERVICE_INCREMENT, {ATTR_ENTITY_ID: entity_id}))
|
||||
|
||||
|
||||
@bind_hass
|
||||
def decrement(hass, entity_id):
|
||||
"""Decrement a counter."""
|
||||
hass.add_job(async_decrement, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_decrement(hass, entity_id):
|
||||
"""Decrement a counter."""
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
DOMAIN, SERVICE_DECREMENT, {ATTR_ENTITY_ID: entity_id}))
|
||||
|
||||
|
||||
@bind_hass
|
||||
def reset(hass, entity_id):
|
||||
"""Reset a counter."""
|
||||
hass.add_job(async_reset, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_reset(hass, entity_id):
|
||||
"""Reset a counter."""
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id}))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up a counter."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
entities = []
|
||||
|
||||
for object_id, cfg in config[DOMAIN].items():
|
||||
if not cfg:
|
||||
cfg = {}
|
||||
|
||||
name = cfg.get(CONF_NAME)
|
||||
initial = cfg.get(CONF_INITIAL)
|
||||
step = cfg.get(CONF_STEP)
|
||||
icon = cfg.get(CONF_ICON)
|
||||
|
||||
entities.append(Counter(object_id, name, initial, step, icon))
|
||||
|
||||
if not entities:
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handler_service(service):
|
||||
"""Handle a call to the counter services."""
|
||||
target_counters = component.async_extract_from_service(service)
|
||||
|
||||
if service.service == SERVICE_INCREMENT:
|
||||
attr = 'async_increment'
|
||||
elif service.service == SERVICE_DECREMENT:
|
||||
attr = 'async_decrement'
|
||||
elif service.service == SERVICE_RESET:
|
||||
attr = 'async_reset'
|
||||
|
||||
tasks = [getattr(counter, attr)() for counter in target_counters]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_INCREMENT, async_handler_service,
|
||||
descriptions[DOMAIN][SERVICE_INCREMENT], SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_DECREMENT, async_handler_service,
|
||||
descriptions[DOMAIN][SERVICE_DECREMENT], SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESET, async_handler_service,
|
||||
descriptions[DOMAIN][SERVICE_RESET], SERVICE_SCHEMA)
|
||||
|
||||
yield from component.async_add_entities(entities)
|
||||
return True
|
||||
|
||||
|
||||
class Counter(Entity):
|
||||
"""Representation of a counter."""
|
||||
|
||||
def __init__(self, object_id, name, initial, step, icon):
|
||||
"""Initialize a counter."""
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self._name = name
|
||||
self._step = step
|
||||
self._state = self._initial = initial
|
||||
self._icon = icon
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""If entity should be polled."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return name of the counter."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to be used for this entity."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current value of the counter."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_INITIAL: self._initial,
|
||||
ATTR_STEP: self._step,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Call when entity about to be added to Home Assistant."""
|
||||
# If not None, we got an initial value.
|
||||
if self._state is not None:
|
||||
return
|
||||
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
self._state = state and state.state == state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_decrement(self):
|
||||
"""Decrement the counter."""
|
||||
self._state -= self._step
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_increment(self):
|
||||
"""Increment a counter."""
|
||||
self._state += self._step
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_reset(self):
|
||||
"""Reset a counter."""
|
||||
self._state = self._initial
|
||||
yield from self.async_update_ha_state()
|
||||
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
This component provides HA cover support for Abode Security System.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.abode/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Abode cover devices."""
|
||||
import abodepy.helpers.constants as CONST
|
||||
|
||||
data = hass.data[ABODE_DOMAIN]
|
||||
|
||||
devices = []
|
||||
for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER):
|
||||
if data.is_excluded(device):
|
||||
continue
|
||||
|
||||
devices.append(AbodeCover(data, device))
|
||||
|
||||
data.devices.extend(devices)
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class AbodeCover(AbodeDevice, CoverDevice):
|
||||
"""Representation of an Abode cover."""
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if cover is closed, else False."""
|
||||
return not self._device.is_open
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Issue close command to cover."""
|
||||
self._device.close_cover()
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Issue open command to cover."""
|
||||
self._device.open_cover()
|
||||
@@ -21,8 +21,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
devices = []
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMCover(hass, conf)
|
||||
new_device.link_homematic()
|
||||
new_device = HMCover(conf)
|
||||
devices.append(new_device)
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
@@ -1,185 +1,213 @@
|
||||
"""
|
||||
Support for KNX covers.
|
||||
Support for KNX/IP covers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.knx/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
|
||||
from homeassistant.helpers.event import async_track_utc_time_change
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, PLATFORM_SCHEMA, ATTR_POSITION, DEVICE_CLASSES_SCHEMA,
|
||||
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, SUPPORT_STOP,
|
||||
SUPPORT_SET_TILT_POSITION
|
||||
)
|
||||
from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice)
|
||||
from homeassistant.const import (CONF_NAME, CONF_DEVICE_CLASS)
|
||||
CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE,
|
||||
SUPPORT_SET_POSITION, SUPPORT_STOP, SUPPORT_SET_TILT_POSITION,
|
||||
ATTR_POSITION, ATTR_TILT_POSITION)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_GETPOSITION_ADDRESS = 'getposition_address'
|
||||
CONF_SETPOSITION_ADDRESS = 'setposition_address'
|
||||
CONF_GETANGLE_ADDRESS = 'getangle_address'
|
||||
CONF_SETANGLE_ADDRESS = 'setangle_address'
|
||||
CONF_STOP = 'stop_address'
|
||||
CONF_UPDOWN = 'updown_address'
|
||||
CONF_MOVE_LONG_ADDRESS = 'move_long_address'
|
||||
CONF_MOVE_SHORT_ADDRESS = 'move_short_address'
|
||||
CONF_POSITION_ADDRESS = 'position_address'
|
||||
CONF_POSITION_STATE_ADDRESS = 'position_state_address'
|
||||
CONF_ANGLE_ADDRESS = 'angle_address'
|
||||
CONF_ANGLE_STATE_ADDRESS = 'angle_state_address'
|
||||
CONF_TRAVELLING_TIME_DOWN = 'travelling_time_down'
|
||||
CONF_TRAVELLING_TIME_UP = 'travelling_time_up'
|
||||
CONF_INVERT_POSITION = 'invert_position'
|
||||
CONF_INVERT_ANGLE = 'invert_angle'
|
||||
|
||||
DEFAULT_TRAVEL_TIME = 25
|
||||
DEFAULT_NAME = 'KNX Cover'
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_UPDOWN): cv.string,
|
||||
vol.Required(CONF_STOP): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_GETPOSITION_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SETPOSITION_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_MOVE_LONG_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_MOVE_SHORT_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_POSITION_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_POSITION_STATE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_ANGLE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_ANGLE_STATE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
|
||||
vol.Inclusive(CONF_GETANGLE_ADDRESS, 'angle'): cv.string,
|
||||
vol.Inclusive(CONF_SETANGLE_ADDRESS, 'angle'): cv.string,
|
||||
vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Create and add an entity based on the configuration."""
|
||||
add_devices([KNXCover(hass, KNXConfig(config))])
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up cover(s) for KNX platform."""
|
||||
if DATA_KNX not in hass.data \
|
||||
or not hass.data[DATA_KNX].initialized:
|
||||
return False
|
||||
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, async_add_devices)
|
||||
else:
|
||||
async_add_devices_config(hass, config, async_add_devices)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class KNXCover(KNXMultiAddressDevice, CoverDevice):
|
||||
"""Representation of a KNX cover. e.g. a rollershutter."""
|
||||
@callback
|
||||
def async_add_devices_discovery(hass, discovery_info, async_add_devices):
|
||||
"""Set up covers for KNX platform configured via xknx.yaml."""
|
||||
entities = []
|
||||
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
device = hass.data[DATA_KNX].xknx.devices[device_name]
|
||||
entities.append(KNXCover(hass, device))
|
||||
async_add_devices(entities)
|
||||
|
||||
def __init__(self, hass, config):
|
||||
|
||||
@callback
|
||||
def async_add_devices_config(hass, config, async_add_devices):
|
||||
"""Set up cover for KNX platform configured within plattform."""
|
||||
import xknx
|
||||
cover = xknx.devices.Cover(
|
||||
hass.data[DATA_KNX].xknx,
|
||||
name=config.get(CONF_NAME),
|
||||
group_address_long=config.get(CONF_MOVE_LONG_ADDRESS),
|
||||
group_address_short=config.get(CONF_MOVE_SHORT_ADDRESS),
|
||||
group_address_position_state=config.get(
|
||||
CONF_POSITION_STATE_ADDRESS),
|
||||
group_address_angle=config.get(CONF_ANGLE_ADDRESS),
|
||||
group_address_angle_state=config.get(CONF_ANGLE_STATE_ADDRESS),
|
||||
group_address_position=config.get(CONF_POSITION_ADDRESS),
|
||||
travel_time_down=config.get(CONF_TRAVELLING_TIME_DOWN),
|
||||
travel_time_up=config.get(CONF_TRAVELLING_TIME_UP),
|
||||
invert_position=config.get(CONF_INVERT_POSITION),
|
||||
invert_angle=config.get(CONF_INVERT_ANGLE))
|
||||
|
||||
hass.data[DATA_KNX].xknx.devices.add(cover)
|
||||
async_add_devices([KNXCover(hass, cover)])
|
||||
|
||||
|
||||
class KNXCover(CoverDevice):
|
||||
"""Representation of a KNX cover."""
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialize the cover."""
|
||||
KNXMultiAddressDevice.__init__(
|
||||
self, hass, config,
|
||||
['updown', 'stop'], # required
|
||||
optional=['setposition', 'getposition',
|
||||
'getangle', 'setangle']
|
||||
)
|
||||
self._device_class = config.config.get(CONF_DEVICE_CLASS)
|
||||
self._invert_position = config.config.get(CONF_INVERT_POSITION)
|
||||
self._invert_angle = config.config.get(CONF_INVERT_ANGLE)
|
||||
self._hass = hass
|
||||
self._current_pos = None
|
||||
self._target_pos = None
|
||||
self._current_tilt = None
|
||||
self._target_tilt = None
|
||||
self._supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | \
|
||||
SUPPORT_SET_POSITION | SUPPORT_STOP
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
|
||||
# Tilt is only supported, if there is a angle get and set address
|
||||
if CONF_SETANGLE_ADDRESS in config.config:
|
||||
_LOGGER.debug("%s: Tilt supported at addresses %s, %s",
|
||||
self.name, config.config.get(CONF_SETANGLE_ADDRESS),
|
||||
config.config.get(CONF_GETANGLE_ADDRESS))
|
||||
self._supported_features = self._supported_features | \
|
||||
SUPPORT_SET_TILT_POSITION
|
||||
self._unsubscribe_auto_updater = None
|
||||
|
||||
@callback
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
def after_update_callback(device):
|
||||
"""Callback after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
yield from self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling is needed for the KNX cover."""
|
||||
return True
|
||||
"""No polling needed within KNX."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return self._supported_features
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | \
|
||||
SUPPORT_SET_POSITION | SUPPORT_STOP
|
||||
if self.device.supports_angle:
|
||||
supported_features |= SUPPORT_SET_TILT_POSITION
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of the cover."""
|
||||
return self.device.current_position()
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self.device.is_closed()
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover.
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
if not self.device.is_closed():
|
||||
yield from self.device.set_down()
|
||||
self.start_auto_updater()
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._current_pos
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
if not self.device.is_open():
|
||||
yield from self.device.set_up()
|
||||
self.start_auto_updater()
|
||||
|
||||
@property
|
||||
def target_position(self):
|
||||
"""Return the position we are trying to reach: 0 - 100."""
|
||||
return self._target_pos
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = kwargs[ATTR_POSITION]
|
||||
yield from self.device.set_position(position)
|
||||
self.start_auto_updater()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
yield from self.device.stop()
|
||||
self.stop_auto_updater()
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
"""Return current position of cover.
|
||||
"""Return current tilt position of cover."""
|
||||
if not self.device.supports_angle:
|
||||
return None
|
||||
return self.device.current_angle()
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._current_tilt
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
if ATTR_TILT_POSITION in kwargs:
|
||||
tilt_position = kwargs[ATTR_TILT_POSITION]
|
||||
yield from self.device.set_angle(tilt_position)
|
||||
|
||||
@property
|
||||
def target_tilt(self):
|
||||
"""Return the tilt angle (in %) we are trying to reach: 0 - 100."""
|
||||
return self._target_tilt
|
||||
def start_auto_updater(self):
|
||||
"""Start the autoupdater to update HASS while cover is moving."""
|
||||
if self._unsubscribe_auto_updater is None:
|
||||
self._unsubscribe_auto_updater = async_track_utc_time_change(
|
||||
self.hass, self.auto_updater_hook)
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Set new target position."""
|
||||
position = kwargs.get(ATTR_POSITION)
|
||||
if position is None:
|
||||
return
|
||||
def stop_auto_updater(self):
|
||||
"""Stop the autoupdater."""
|
||||
if self._unsubscribe_auto_updater is not None:
|
||||
self._unsubscribe_auto_updater()
|
||||
self._unsubscribe_auto_updater = None
|
||||
|
||||
if self._invert_position:
|
||||
position = 100-position
|
||||
@callback
|
||||
def auto_updater_hook(self, now):
|
||||
"""Callback for autoupdater."""
|
||||
# pylint: disable=unused-argument
|
||||
self.async_schedule_update_ha_state()
|
||||
if self.device.position_reached():
|
||||
self.stop_auto_updater()
|
||||
|
||||
self._target_pos = position
|
||||
self.set_percentage('setposition', position)
|
||||
_LOGGER.debug("%s: Set target position to %d", self.name, position)
|
||||
|
||||
def update(self):
|
||||
"""Update device state."""
|
||||
super().update()
|
||||
value = self.get_percentage('getposition')
|
||||
if value is not None:
|
||||
self._current_pos = value
|
||||
if self._invert_position:
|
||||
self._current_pos = 100-value
|
||||
_LOGGER.debug("%s: position = %d", self.name, value)
|
||||
|
||||
if self._supported_features & SUPPORT_SET_TILT_POSITION:
|
||||
value = self.get_percentage('getangle')
|
||||
if value is not None:
|
||||
self._current_tilt = value
|
||||
if self._invert_angle:
|
||||
self._current_tilt = 100-value
|
||||
_LOGGER.debug("%s: tilt = %d", self.name, value)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
_LOGGER.debug("%s: open: updown = 0", self.name)
|
||||
self.set_int_value('updown', 0)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
_LOGGER.debug("%s: open: updown = 1", self.name)
|
||||
self.set_int_value('updown', 1)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover movement."""
|
||||
_LOGGER.debug("%s: stop: stop = 1", self.name)
|
||||
self.set_int_value('stop', 1)
|
||||
|
||||
def set_cover_tilt_position(self, tilt_position, **kwargs):
|
||||
"""Move the cover til to a specific position."""
|
||||
if self._invert_angle:
|
||||
tilt_position = 100-tilt_position
|
||||
|
||||
self._target_tilt = round(tilt_position, -1)
|
||||
self.set_percentage('setangle', tilt_position)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return self._device_class
|
||||
self.hass.add_job(self.device.auto_stop_if_necessary())
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"""
|
||||
Support for Lutron Caseta SerenaRollerShade.
|
||||
Support for Lutron Caseta shades.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.lutron_caseta/
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION)
|
||||
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION,
|
||||
ATTR_POSITION, DOMAIN)
|
||||
from homeassistant.components.lutron_caseta import (
|
||||
LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice)
|
||||
|
||||
@@ -19,11 +19,10 @@ DEPENDENCIES = ['lutron_caseta']
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Lutron Caseta Serena shades as a cover device."""
|
||||
"""Set up the Lutron Caseta shades as a cover device."""
|
||||
devs = []
|
||||
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
|
||||
cover_devices = bridge.get_devices_by_types(["SerenaRollerShade",
|
||||
"SerenaHoneycombShade"])
|
||||
cover_devices = bridge.get_devices_by_domain(DOMAIN)
|
||||
for cover_device in cover_devices:
|
||||
dev = LutronCasetaCover(cover_device, bridge)
|
||||
devs.append(dev)
|
||||
@@ -32,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
|
||||
"""Representation of a Lutron Serena shade."""
|
||||
"""Representation of a Lutron shade."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
@@ -42,24 +41,26 @@ class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self._state["current_state"] < 1
|
||||
return self._state['current_state'] < 1
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of cover."""
|
||||
return self._state["current_state"]
|
||||
return self._state['current_state']
|
||||
|
||||
def close_cover(self):
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self._smartbridge.set_value(self._device_id, 0)
|
||||
|
||||
def open_cover(self):
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._smartbridge.set_value(self._device_id, 100)
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the roller shutter to a specific position."""
|
||||
self._smartbridge.set_value(self._device_id, position)
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Move the shade to a specific position."""
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = kwargs[ATTR_POSITION]
|
||||
self._smartbridge.set_value(self._device_id, position)
|
||||
|
||||
def update(self):
|
||||
"""Call when forcing a refresh of the device."""
|
||||
|
||||
@@ -178,7 +178,7 @@ class MqttCover(CoverDevice):
|
||||
|
||||
level = self.find_percentage_in_range(float(payload))
|
||||
self._tilt_value = level
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
@@ -203,7 +203,7 @@ class MqttCover(CoverDevice):
|
||||
payload)
|
||||
return
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._state_topic is None:
|
||||
# Force into optimistic mode.
|
||||
@@ -275,7 +275,7 @@ class MqttCover(CoverDevice):
|
||||
if self._optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._state = False
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
@@ -289,7 +289,7 @@ class MqttCover(CoverDevice):
|
||||
if self._optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._state = True
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
@@ -309,7 +309,7 @@ class MqttCover(CoverDevice):
|
||||
self._retain)
|
||||
if self._tilt_optimistic:
|
||||
self._tilt_value = self._tilt_open_position
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover_tilt(self, **kwargs):
|
||||
@@ -319,7 +319,7 @@ class MqttCover(CoverDevice):
|
||||
self._retain)
|
||||
if self._tilt_optimistic:
|
||||
self._tilt_value = self._tilt_closed_position
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_tilt_position(self, **kwargs):
|
||||
|
||||
@@ -16,7 +16,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Set up the RFXtrx cover."""
|
||||
import RFXtrx as rfxtrxmod
|
||||
|
||||
covers = rfxtrx.get_devices_from_config(config, RfxtrxCover, hass)
|
||||
covers = rfxtrx.get_devices_from_config(config, RfxtrxCover)
|
||||
add_devices_callback(covers)
|
||||
|
||||
def cover_update(event):
|
||||
@@ -26,7 +26,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
not event.device.known_to_be_rollershutter:
|
||||
return
|
||||
|
||||
new_device = rfxtrx.get_new_device(event, config, RfxtrxCover, hass)
|
||||
new_device = rfxtrx.get_new_device(event, config, RfxtrxCover)
|
||||
if new_device:
|
||||
add_devices_callback([new_device])
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ class CoverTemplate(CoverDevice):
|
||||
@callback
|
||||
def template_cover_state_listener(entity, old_state, new_state):
|
||||
"""Handle target device state changes."""
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@callback
|
||||
def template_cover_startup(event):
|
||||
@@ -205,7 +205,7 @@ class CoverTemplate(CoverDevice):
|
||||
async_track_state_change(
|
||||
self.hass, self._entities, template_cover_state_listener)
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, template_cover_startup)
|
||||
@@ -271,7 +271,7 @@ class CoverTemplate(CoverDevice):
|
||||
yield from self._position_script.async_run({"position": 100})
|
||||
if self._optimistic:
|
||||
self._position = 100
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
@@ -282,7 +282,7 @@ class CoverTemplate(CoverDevice):
|
||||
yield from self._position_script.async_run({"position": 0})
|
||||
if self._optimistic:
|
||||
self._position = 0
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
@@ -297,7 +297,7 @@ class CoverTemplate(CoverDevice):
|
||||
yield from self._position_script.async_run(
|
||||
{"position": self._position})
|
||||
if self._optimistic:
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_open_cover_tilt(self, **kwargs):
|
||||
@@ -305,7 +305,7 @@ class CoverTemplate(CoverDevice):
|
||||
self._tilt_value = 100
|
||||
yield from self._tilt_script.async_run({"tilt": self._tilt_value})
|
||||
if self._tilt_optimistic:
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover_tilt(self, **kwargs):
|
||||
@@ -314,7 +314,7 @@ class CoverTemplate(CoverDevice):
|
||||
yield from self._tilt_script.async_run(
|
||||
{"tilt": self._tilt_value})
|
||||
if self._tilt_optimistic:
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_tilt_position(self, **kwargs):
|
||||
@@ -322,7 +322,7 @@ class CoverTemplate(CoverDevice):
|
||||
self._tilt_value = kwargs[ATTR_TILT_POSITION]
|
||||
yield from self._tilt_script.async_run({"tilt": self._tilt_value})
|
||||
if self._tilt_optimistic:
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
|
||||
+8
-7
@@ -2,7 +2,8 @@
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.xiaomi import (PY_XIAOMI_GATEWAY, XiaomiDevice)
|
||||
from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY,
|
||||
XiaomiDevice)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,10 +25,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class XiaomiGenericCover(XiaomiDevice, CoverDevice):
|
||||
"""Representation of a XiaomiPlug."""
|
||||
"""Representation of a XiaomiGenericCover."""
|
||||
|
||||
def __init__(self, device, name, data_key, xiaomi_hub):
|
||||
"""Initialize the XiaomiPlug."""
|
||||
"""Initialize the XiaomiGenericCover."""
|
||||
self._data_key = data_key
|
||||
self._pos = 0
|
||||
XiaomiDevice.__init__(self, device, name, xiaomi_hub)
|
||||
@@ -44,19 +45,19 @@ class XiaomiGenericCover(XiaomiDevice, CoverDevice):
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self._write_to_hub(self._sid, self._data_key['status'], 'close')
|
||||
self._write_to_hub(self._sid, **{self._data_key['status']: 'close'})
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._write_to_hub(self._sid, self._data_key['status'], 'open')
|
||||
self._write_to_hub(self._sid, **{self._data_key['status']: 'open'})
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self._write_to_hub(self._sid, self._data_key['status'], 'stop')
|
||||
self._write_to_hub(self._sid, **{self._data_key['status']: 'stop'})
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
self._write_to_hub(self._sid, self._data_key['pos'], str(position))
|
||||
self._write_to_hub(self._sid, **{self._data_key['pos']: str(position)})
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
@@ -19,9 +19,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
|
||||
_DEVICES_REGEX = re.compile(
|
||||
r'(?P<name>([^\s]+))\s+' +
|
||||
r'(?P<name>([^\s]+)?)\s+' +
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' +
|
||||
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s+')
|
||||
r'(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))\s+')
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
REQUIREMENTS = ['aioautomatic==0.6.0']
|
||||
REQUIREMENTS = ['aioautomatic==0.6.3']
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -205,6 +205,7 @@ class AutomaticData(object):
|
||||
self.hass = hass
|
||||
self.devices = devices
|
||||
self.vehicle_info = {}
|
||||
self.vehicle_seen = {}
|
||||
self.client = client
|
||||
self.session = session
|
||||
self.async_see = async_see
|
||||
@@ -236,6 +237,14 @@ class AutomaticData(object):
|
||||
return
|
||||
yield from self.get_vehicle_info(vehicle)
|
||||
|
||||
if event.created_at < self.vehicle_seen[event.vehicle.id]:
|
||||
# Skip events received out of order
|
||||
_LOGGER.debug("Skipping out of order event. Event Created %s. "
|
||||
"Last seen event: %s.", event.created_at,
|
||||
self.vehicle_seen[event.vehicle.id])
|
||||
return
|
||||
self.vehicle_seen[event.vehicle.id] = event.created_at
|
||||
|
||||
kwargs = self.vehicle_info[event.vehicle.id]
|
||||
if kwargs is None:
|
||||
# Ignored device
|
||||
@@ -323,15 +332,17 @@ class AutomaticData(object):
|
||||
if self.devices is not None and name not in self.devices:
|
||||
self.vehicle_info[vehicle.id] = None
|
||||
return
|
||||
else:
|
||||
self.vehicle_info[vehicle.id] = kwargs = {
|
||||
ATTR_DEV_ID: vehicle.id,
|
||||
ATTR_HOST_NAME: name,
|
||||
ATTR_MAC: vehicle.id,
|
||||
ATTR_ATTRIBUTES: {
|
||||
ATTR_FUEL_LEVEL: vehicle.fuel_level_percent,
|
||||
}
|
||||
|
||||
self.vehicle_info[vehicle.id] = kwargs = {
|
||||
ATTR_DEV_ID: vehicle.id,
|
||||
ATTR_HOST_NAME: name,
|
||||
ATTR_MAC: vehicle.id,
|
||||
ATTR_ATTRIBUTES: {
|
||||
ATTR_FUEL_LEVEL: vehicle.fuel_level_percent,
|
||||
}
|
||||
}
|
||||
self.vehicle_seen[vehicle.id] = \
|
||||
vehicle.updated_at or vehicle.created_at
|
||||
|
||||
if vehicle.latest_location is not None:
|
||||
location = vehicle.latest_location
|
||||
@@ -352,4 +363,7 @@ class AutomaticData(object):
|
||||
kwargs[ATTR_GPS] = (location.lat, location.lon)
|
||||
kwargs[ATTR_GPS_ACCURACY] = location.accuracy_m
|
||||
|
||||
if trips[0].ended_at >= self.vehicle_seen[vehicle.id]:
|
||||
self.vehicle_seen[vehicle.id] = trips[0].ended_at
|
||||
|
||||
return kwargs
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Support for the Geofency platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.geofency/
|
||||
"""
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE, ATTR_LONGITUDE, HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import slugify
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
BEACON_DEV_PREFIX = 'beacon'
|
||||
CONF_MOBILE_BEACONS = 'mobile_beacons'
|
||||
|
||||
LOCATION_ENTRY = '1'
|
||||
LOCATION_EXIT = '0'
|
||||
|
||||
URL = '/api/geofency'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MOBILE_BEACONS): vol.All(
|
||||
cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Set up an endpoint for the Geofency application."""
|
||||
mobile_beacons = config.get(CONF_MOBILE_BEACONS) or []
|
||||
|
||||
hass.http.register_view(GeofencyView(see, mobile_beacons))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class GeofencyView(HomeAssistantView):
|
||||
"""View to handle Geofency requests."""
|
||||
|
||||
url = URL
|
||||
name = 'api:geofency'
|
||||
|
||||
def __init__(self, see, mobile_beacons):
|
||||
"""Initialize Geofency url endpoints."""
|
||||
self.see = see
|
||||
self.mobile_beacons = [slugify(beacon) for beacon in mobile_beacons]
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle Geofency requests."""
|
||||
data = yield from request.post()
|
||||
hass = request.app['hass']
|
||||
|
||||
data = self._validate_data(data)
|
||||
if not data:
|
||||
return ("Invalid data", HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if self._is_mobile_beacon(data):
|
||||
return (yield from self._set_location(hass, data, None))
|
||||
else:
|
||||
if data['entry'] == LOCATION_ENTRY:
|
||||
location_name = data['name']
|
||||
else:
|
||||
location_name = STATE_NOT_HOME
|
||||
|
||||
return (yield from self._set_location(hass, data, location_name))
|
||||
|
||||
@staticmethod
|
||||
def _validate_data(data):
|
||||
"""Validate POST payload."""
|
||||
data = data.copy()
|
||||
|
||||
required_attributes = ['address', 'device', 'entry',
|
||||
'latitude', 'longitude', 'name']
|
||||
|
||||
valid = True
|
||||
for attribute in required_attributes:
|
||||
if attribute not in data:
|
||||
valid = False
|
||||
_LOGGER.error("'%s' not specified in message", attribute)
|
||||
|
||||
if not valid:
|
||||
return False
|
||||
|
||||
data['address'] = data['address'].replace('\n', ' ')
|
||||
data['device'] = slugify(data['device'])
|
||||
data['name'] = slugify(data['name'])
|
||||
|
||||
data[ATTR_LATITUDE] = float(data[ATTR_LATITUDE])
|
||||
data[ATTR_LONGITUDE] = float(data[ATTR_LONGITUDE])
|
||||
|
||||
return data
|
||||
|
||||
def _is_mobile_beacon(self, data):
|
||||
"""Check if we have a mobile beacon."""
|
||||
return 'beaconUUID' in data and data['name'] in self.mobile_beacons
|
||||
|
||||
@staticmethod
|
||||
def _device_name(data):
|
||||
"""Return name of device tracker."""
|
||||
if 'beaconUUID' in data:
|
||||
return "{}_{}".format(BEACON_DEV_PREFIX, data['name'])
|
||||
else:
|
||||
return data['device']
|
||||
|
||||
@asyncio.coroutine
|
||||
def _set_location(self, hass, data, location_name):
|
||||
"""Fire HA event to set location."""
|
||||
device = self._device_name(data)
|
||||
|
||||
yield from hass.async_add_job(
|
||||
partial(self.see, dev_id=device,
|
||||
gps=(data[ATTR_LATITUDE], data[ATTR_LONGITUDE]),
|
||||
location_name=location_name,
|
||||
attributes=data))
|
||||
|
||||
return "Setting location for {}".format(device)
|
||||
@@ -307,12 +307,15 @@ class Icloud(DeviceScanner):
|
||||
self.api.authenticate()
|
||||
|
||||
currentminutes = dt_util.now().hour * 60 + dt_util.now().minute
|
||||
for devicename in self.devices:
|
||||
interval = self._intervals.get(devicename, 1)
|
||||
if ((currentminutes % interval == 0) or
|
||||
(interval > 10 and
|
||||
currentminutes % interval in [2, 4])):
|
||||
self.update_device(devicename)
|
||||
try:
|
||||
for devicename in self.devices:
|
||||
interval = self._intervals.get(devicename, 1)
|
||||
if ((currentminutes % interval == 0) or
|
||||
(interval > 10 and
|
||||
currentminutes % interval in [2, 4])):
|
||||
self.update_device(devicename)
|
||||
except ValueError:
|
||||
_LOGGER.debug("iCloud API returned an error")
|
||||
|
||||
def determine_interval(self, devicename, latitude, longitude, battery):
|
||||
"""Calculate new interval."""
|
||||
@@ -397,7 +400,7 @@ class Icloud(DeviceScanner):
|
||||
self.see(**kwargs)
|
||||
self.seen_devices[devicename] = True
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.error('No iCloud Devices found!')
|
||||
_LOGGER.error("No iCloud Devices found")
|
||||
|
||||
def lost_iphone(self, devicename):
|
||||
"""Call the lost iPhone function if the device is found."""
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Support for Zyxel Keenetic NDMS2 based routers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.keenetic_ndms2/
|
||||
"""
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Interface name to track devices for. Most likely one will not need to
|
||||
# change it from default 'Home'. This is needed not to track Guest WI-FI-
|
||||
# clients and router itself
|
||||
CONF_INTERFACE = 'interface'
|
||||
|
||||
DEFAULT_INTERFACE = 'Home'
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def get_scanner(_hass, config):
|
||||
"""Validate the configuration and return a Nmap scanner."""
|
||||
scanner = KeeneticNDMS2DeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
Device = namedtuple('Device', ['mac', 'name'])
|
||||
|
||||
|
||||
class KeeneticNDMS2DeviceScanner(DeviceScanner):
|
||||
"""This class scans for devices using keenetic NDMS2 web interface."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.last_results = []
|
||||
|
||||
self._url = 'http://%s/rci/show/ip/arp' % config[CONF_HOST]
|
||||
self._interface = config[CONF_INTERFACE]
|
||||
|
||||
self._username = config.get(CONF_USERNAME)
|
||||
self._password = config.get(CONF_PASSWORD)
|
||||
|
||||
self.success_init = self._update_info()
|
||||
_LOGGER.info("Scanner initialized")
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return [device.mac for device in self.last_results]
|
||||
|
||||
def get_device_name(self, mac):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
filter_named = [device.name for device in self.last_results
|
||||
if device.mac == mac]
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
return None
|
||||
|
||||
def _update_info(self):
|
||||
"""Get ARP from keenetic router."""
|
||||
_LOGGER.info("Fetching...")
|
||||
|
||||
last_results = []
|
||||
|
||||
# doing a request
|
||||
try:
|
||||
from requests.auth import HTTPDigestAuth
|
||||
res = requests.get(self._url, timeout=10, auth=HTTPDigestAuth(
|
||||
self._username, self._password
|
||||
))
|
||||
except requests.exceptions.Timeout:
|
||||
_LOGGER.error(
|
||||
"Connection to the router timed out at URL %s", self._url)
|
||||
return False
|
||||
if res.status_code != 200:
|
||||
_LOGGER.error(
|
||||
"Connection failed with http code %s", res.status_code)
|
||||
return False
|
||||
try:
|
||||
result = res.json()
|
||||
except ValueError:
|
||||
# If json decoder could not parse the response
|
||||
_LOGGER.error("Failed to parse response from router")
|
||||
return False
|
||||
|
||||
# parsing response
|
||||
for info in result:
|
||||
if info.get('interface') != self._interface:
|
||||
continue
|
||||
mac = info.get('mac')
|
||||
name = info.get('name')
|
||||
# No address = no item :)
|
||||
if mac is None:
|
||||
continue
|
||||
|
||||
last_results.append(Device(mac.upper(), name))
|
||||
|
||||
self.last_results = last_results
|
||||
|
||||
_LOGGER.info("Request successful")
|
||||
return True
|
||||
@@ -42,7 +42,7 @@ VALIDATE_WAYPOINTS = 'waypoints'
|
||||
|
||||
WAYPOINT_LAT_KEY = 'lat'
|
||||
WAYPOINT_LON_KEY = 'lon'
|
||||
WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoint'
|
||||
WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoints'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Support for the Tesla platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.tesla/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.util import slugify
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['tesla']
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Set up the Tesla tracker."""
|
||||
TeslaDeviceTracker(
|
||||
hass, config, see,
|
||||
hass.data[TESLA_DOMAIN]['devices']['devices_tracker'])
|
||||
return True
|
||||
|
||||
|
||||
class TeslaDeviceTracker(object):
|
||||
"""A class representing a Tesla device."""
|
||||
|
||||
def __init__(self, hass, config, see, tesla_devices):
|
||||
"""Initialize the Tesla device scanner."""
|
||||
self.hass = hass
|
||||
self.see = see
|
||||
self.devices = tesla_devices
|
||||
self._update_info()
|
||||
|
||||
track_utc_time_change(
|
||||
self.hass, self._update_info, second=range(0, 60, 30))
|
||||
|
||||
def _update_info(self, now=None):
|
||||
"""Update the device info."""
|
||||
for device in self.devices:
|
||||
device.update()
|
||||
name = device.name
|
||||
_LOGGER.debug("Updating device position: %s", name)
|
||||
dev_id = slugify(device.uniq_name)
|
||||
location = device.get_location()
|
||||
lat = location['latitude']
|
||||
lon = location['longitude']
|
||||
attrs = {
|
||||
'trackr_id': dev_id,
|
||||
'id': dev_id,
|
||||
'name': name
|
||||
}
|
||||
self.see(
|
||||
dev_id=dev_id, host_name=name,
|
||||
gps=(lat, lon), attributes=attrs
|
||||
)
|
||||
@@ -20,11 +20,12 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
return
|
||||
|
||||
vin, _ = discovery_info
|
||||
vehicle = hass.data[DATA_KEY].vehicles[vin]
|
||||
voc = hass.data[DATA_KEY]
|
||||
vehicle = voc.vehicles[vin]
|
||||
|
||||
def see_vehicle(vehicle):
|
||||
"""Handle the reporting of the vehicle position."""
|
||||
host_name = vehicle.registration_number
|
||||
host_name = voc.vehicle_name(vehicle)
|
||||
dev_id = 'volvo_{}'.format(slugify(host_name))
|
||||
see(dev_id=dev_id,
|
||||
host_name=host_name,
|
||||
|
||||
@@ -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
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['netdisco==1.1.0']
|
||||
REQUIREMENTS = ['netdisco==1.2.0']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
@@ -34,6 +34,7 @@ SERVICE_HASSIO = 'hassio'
|
||||
SERVICE_AXIS = 'axis'
|
||||
SERVICE_APPLE_TV = 'apple_tv'
|
||||
SERVICE_WINK = 'wink'
|
||||
SERVICE_XIAOMI_GW = 'xiaomi_gw'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||
@@ -44,6 +45,7 @@ SERVICE_HANDLERS = {
|
||||
SERVICE_AXIS: ('axis', None),
|
||||
SERVICE_APPLE_TV: ('apple_tv', None),
|
||||
SERVICE_WINK: ('wink', None),
|
||||
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
|
||||
'philips_hue': ('light', 'hue'),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
@@ -100,6 +102,7 @@ def async_setup(hass, config):
|
||||
|
||||
# We do not know how to handle this service.
|
||||
if not comp_plat:
|
||||
logger.info("Unknown service discovered: %s %s", service, info)
|
||||
return
|
||||
|
||||
discovery_hash = json.dumps([service, info], sort_keys=True)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Support for a DoorBird video doorbell."""
|
||||
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['DoorBirdPy==0.0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'doorbird'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the DoorBird component."""
|
||||
device_ip = config[DOMAIN].get(CONF_HOST)
|
||||
username = config[DOMAIN].get(CONF_USERNAME)
|
||||
password = config[DOMAIN].get(CONF_PASSWORD)
|
||||
|
||||
from doorbirdpy import DoorBird
|
||||
device = DoorBird(device_ip, username, password)
|
||||
status = device.ready()
|
||||
|
||||
if status[0]:
|
||||
_LOGGER.info("Connected to DoorBird at %s as %s", device_ip, username)
|
||||
hass.data[DOMAIN] = device
|
||||
return True
|
||||
elif status[1] == 401:
|
||||
_LOGGER.error("Authorization rejected by DoorBird at %s", device_ip)
|
||||
return False
|
||||
else:
|
||||
_LOGGER.error("Could not connect to DoorBird at %s: Error %s",
|
||||
device_ip, str(status[1]))
|
||||
return False
|
||||
@@ -15,7 +15,7 @@ from homeassistant.helpers import discovery
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['python-ecobee-api==0.0.8']
|
||||
REQUIREMENTS = ['python-ecobee-api==0.0.9']
|
||||
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -209,7 +209,7 @@ class EightSleepUserEntity(Entity):
|
||||
@callback
|
||||
def async_eight_user_update():
|
||||
"""Update callback."""
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_USER, async_eight_user_update)
|
||||
@@ -233,7 +233,7 @@ class EightSleepHeatEntity(Entity):
|
||||
@callback
|
||||
def async_eight_heat_update():
|
||||
"""Update callback."""
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_HEAT, async_eight_heat_update)
|
||||
|
||||
@@ -129,7 +129,7 @@ class Config(object):
|
||||
|
||||
if self.type == TYPE_ALEXA:
|
||||
_LOGGER.warning("Alexa type is deprecated and will be removed in a"
|
||||
"future version")
|
||||
" future version")
|
||||
|
||||
# Get the IP address that will be passed to the Echo during discovery
|
||||
self.host_ip_addr = conf.get(CONF_HOST_IP)
|
||||
|
||||
@@ -136,7 +136,7 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
|
||||
# because the data object has not been initialized
|
||||
continue
|
||||
|
||||
if "M-SEARCH" in data.decode('utf-8'):
|
||||
if "M-SEARCH" in data.decode('utf-8', errors='ignore'):
|
||||
# SSDP M-SEARCH method received, respond to it with our info
|
||||
resp_socket = socket.socket(
|
||||
socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
@@ -78,6 +78,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the MQTT fan platform."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
|
||||
async_add_devices([MqttFan(
|
||||
config.get(CONF_NAME),
|
||||
{
|
||||
@@ -160,7 +163,7 @@ class MqttFan(FanEntity):
|
||||
self._state = True
|
||||
elif payload == self._payload[STATE_OFF]:
|
||||
self._state = False
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
@@ -177,7 +180,7 @@ class MqttFan(FanEntity):
|
||||
self._speed = SPEED_MEDIUM
|
||||
elif payload == self._payload[SPEED_HIGH]:
|
||||
self._speed = SPEED_HIGH
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_SPEED_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
@@ -193,7 +196,7 @@ class MqttFan(FanEntity):
|
||||
self._oscillation = True
|
||||
elif payload == self._payload[OSCILLATE_OFF_PAYLOAD]:
|
||||
self._oscillation = False
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
@@ -287,7 +290,7 @@ class MqttFan(FanEntity):
|
||||
|
||||
if self._optimistic_speed:
|
||||
self._speed = speed
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_oscillate(self, oscillating: bool) -> None:
|
||||
@@ -309,4 +312,4 @@ class MqttFan(FanEntity):
|
||||
|
||||
if self._optimistic_oscillation:
|
||||
self._oscillation = oscillating
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@@ -242,7 +242,7 @@ class FFmpegBase(Entity):
|
||||
def async_start_handle(event):
|
||||
"""Start FFmpeg process."""
|
||||
yield from self._async_start_ffmpeg(None)
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, async_start_handle)
|
||||
|
||||
@@ -28,6 +28,7 @@ URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
|
||||
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static/')
|
||||
|
||||
ATTR_THEMES = 'themes'
|
||||
ATTR_EXTRA_HTML_URL = 'extra_html_url'
|
||||
DEFAULT_THEME_COLOR = '#03A9F4'
|
||||
MANIFEST_JSON = {
|
||||
'background_color': '#FFFFFF',
|
||||
@@ -50,6 +51,7 @@ for size in (192, 384, 512, 1024):
|
||||
})
|
||||
|
||||
DATA_PANELS = 'frontend_panels'
|
||||
DATA_EXTRA_HTML_URL = 'frontend_extra_html_url'
|
||||
DATA_INDEX_VIEW = 'frontend_index_view'
|
||||
DATA_THEMES = 'frontend_themes'
|
||||
DATA_DEFAULT_THEME = 'frontend_default_theme'
|
||||
@@ -66,6 +68,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_THEMES): vol.Schema({
|
||||
cv.string: {cv.string: cv.string}
|
||||
}),
|
||||
vol.Optional(ATTR_EXTRA_HTML_URL):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -105,14 +109,13 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
||||
|
||||
component_name: name of the web component
|
||||
path: path to the HTML of the web component
|
||||
(required unless url is provided)
|
||||
md5: the md5 hash of the web component (for versioning, optional)
|
||||
sidebar_title: title to show in the sidebar (optional)
|
||||
sidebar_icon: icon to show next to title in sidebar (optional)
|
||||
url_path: name to use in the url (defaults to component_name)
|
||||
url: for the web component (for dev environment, optional)
|
||||
url: for the web component (optional)
|
||||
config: config to be passed into the web component
|
||||
|
||||
Warning: this API will probably change. Use at own risk.
|
||||
"""
|
||||
panels = hass.data.get(DATA_PANELS)
|
||||
if panels is None:
|
||||
@@ -123,14 +126,16 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
||||
|
||||
if url_path in panels:
|
||||
_LOGGER.warning("Overwriting component %s", url_path)
|
||||
if not os.path.isfile(path):
|
||||
_LOGGER.error(
|
||||
"Panel %s component does not exist: %s", component_name, path)
|
||||
return
|
||||
|
||||
if md5 is None:
|
||||
with open(path) as fil:
|
||||
md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest()
|
||||
if url is None:
|
||||
if not os.path.isfile(path):
|
||||
_LOGGER.error(
|
||||
"Panel %s component does not exist: %s", component_name, path)
|
||||
return
|
||||
|
||||
if md5 is None:
|
||||
with open(path) as fil:
|
||||
md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest()
|
||||
|
||||
data = {
|
||||
'url_path': url_path,
|
||||
@@ -169,6 +174,15 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
||||
'get', '/{}/{{extra:.+}}'.format(url_path), index_view.get)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def add_extra_html_url(hass, url):
|
||||
"""Register extra html url to load."""
|
||||
url_set = hass.data.get(DATA_EXTRA_HTML_URL)
|
||||
if url_set is None:
|
||||
url_set = hass.data[DATA_EXTRA_HTML_URL] = set()
|
||||
url_set.add(url)
|
||||
|
||||
|
||||
def add_manifest_json_key(key, val):
|
||||
"""Add a keyval to the manifest.json."""
|
||||
MANIFEST_JSON[key] = val
|
||||
@@ -208,6 +222,9 @@ def setup(hass, config):
|
||||
else:
|
||||
hass.data[DATA_PANELS] = {}
|
||||
|
||||
if DATA_EXTRA_HTML_URL not in hass.data:
|
||||
hass.data[DATA_EXTRA_HTML_URL] = set()
|
||||
|
||||
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
|
||||
|
||||
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
|
||||
@@ -217,6 +234,9 @@ def setup(hass, config):
|
||||
themes = config.get(DOMAIN, {}).get(ATTR_THEMES)
|
||||
setup_themes(hass, themes)
|
||||
|
||||
for url in config.get(DOMAIN, {}).get(ATTR_EXTRA_HTML_URL, []):
|
||||
add_extra_html_url(hass, url)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -362,7 +382,9 @@ class IndexView(HomeAssistantView):
|
||||
compatibility_url=compatibility_url, no_auth=no_auth,
|
||||
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
|
||||
panel_url=panel_url, panels=hass.data[DATA_PANELS],
|
||||
dev_mode=request.app[KEY_DEVELOPMENT])
|
||||
dev_mode=request.app[KEY_DEVELOPMENT],
|
||||
theme_color=MANIFEST_JSON['theme_color'],
|
||||
extra_urls=hass.data[DATA_EXTRA_HTML_URL])
|
||||
|
||||
return web.Response(text=resp, content_type='text/html')
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
|
||||
<meta name='mobile-web-app-capable' content='yes'>
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||
<meta name='theme-color' content='#03a9f4'>
|
||||
<meta name='theme-color' content='{{ theme_color }}'>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', 'Noto', sans-serif;
|
||||
@@ -92,23 +92,25 @@
|
||||
{% if not dev_mode %}
|
||||
<script src='/static/custom-elements-es5-adapter.js'></script>
|
||||
{% endif %}
|
||||
<link rel='import' href='{{ ui_url }}' onerror='initError()'>
|
||||
{% if panel_url -%}
|
||||
<link rel='import' href='{{ panel_url }}' onerror='initError()' async>
|
||||
{% endif -%}
|
||||
<link rel='import' href='{{ icons_url }}' async>
|
||||
<script>
|
||||
var webComponentsSupported = (
|
||||
'registerElement' in document &&
|
||||
'customElements' in window &&
|
||||
'import' in document.createElement('link') &&
|
||||
'content' in document.createElement('template'));
|
||||
if (!webComponentsSupported) {
|
||||
var e = document.createElement('script');
|
||||
e.async = true;
|
||||
e.onerror = initError;
|
||||
e.src = '/static/webcomponents-lite.js';
|
||||
document.head.appendChild(e);
|
||||
}
|
||||
</script>
|
||||
<link rel='import' href='{{ ui_url }}' onerror='initError()'>
|
||||
{% if panel_url -%}
|
||||
<link rel='import' href='{{ panel_url }}' onerror='initError()' async>
|
||||
{% endif -%}
|
||||
<link rel='import' href='{{ icons_url }}' async>
|
||||
{% for extra_url in extra_urls -%}
|
||||
<link rel='import' href='{{ extra_url }}' async>
|
||||
{% endfor -%}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
FINGERPRINTS = {
|
||||
"compatibility.js": "1686167ff210e001f063f5c606b2e74b",
|
||||
"core.js": "2a7d01e45187c7d4635da05065b5e54e",
|
||||
"frontend.html": "6c8192a4393c9e83516dc8177b75c23d",
|
||||
"mdi.html": "e91f61a039ed0a9936e7ee5360da3870",
|
||||
"frontend.html": "7e13ce36d3141182a62a5b061e87e77a",
|
||||
"mdi.html": "89074face5529f5fe6fbae49ecb3e88b",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-config.html": "bd20a3b11b46522e3c705a0b6a72b9dc",
|
||||
"panels/ha-panel-config.html": "61f65e75e39368e07441d7d6a4e36ae3",
|
||||
"panels/ha-panel-dev-event.html": "d409e7ab537d9fe629126d122345279c",
|
||||
"panels/ha-panel-dev-info.html": "b0e55eb657fd75f21aba2426ac0cedc0",
|
||||
"panels/ha-panel-dev-mqtt.html": "94b222b013a98583842de3e72d5888c6",
|
||||
"panels/ha-panel-dev-service.html": "422b2c181ee0713fa31d45a64e605baf",
|
||||
"panels/ha-panel-dev-state.html": "7948d3dba058f31517d880df8ed0e857",
|
||||
"panels/ha-panel-dev-template.html": "f47b6910d8e4880e22cc508ca452f9b6",
|
||||
"panels/ha-panel-dev-template.html": "928e7b81b9c113b70edc9f4a1d051827",
|
||||
"panels/ha-panel-hassio.html": "b46e7619f3c355f872d5370741d89f6a",
|
||||
"panels/ha-panel-history.html": "fe2daac10a14f51fa3eb7d23978df1f7",
|
||||
"panels/ha-panel-iframe.html": "56930204d6e067a3d600cf030f4b34c8",
|
||||
"panels/ha-panel-kiosk.html": "b40aa5cb52dd7675bea744afcf9eebf8",
|
||||
"panels/ha-panel-logbook.html": "771afdcf48dc7e308b0282417d2e02d8",
|
||||
"panels/ha-panel-mailbox.html": "a8cca44ca36553e91565e3c894ea6323",
|
||||
"panels/ha-panel-map.html": "c2544fff3eedb487d44105cf94b335ec",
|
||||
"panels/ha-panel-map.html": "565db019147162080c21af962afc097f",
|
||||
"panels/ha-panel-shopping-list.html": "d8cfd0ecdb3aa6214c0f6908c34c7141"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user