mirror of
https://github.com/home-assistant/core.git
synced 2025-07-07 21:37:07 +00:00
commit
e28170a0a6
24
.coveragerc
24
.coveragerc
@ -102,6 +102,9 @@ omit =
|
||||
homeassistant/components/egardia.py
|
||||
homeassistant/components/*/egardia.py
|
||||
|
||||
homeassistant/components/elkm1/*
|
||||
homeassistant/components/*/elkm1.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
@ -245,6 +248,9 @@ omit =
|
||||
homeassistant/components/opencv.py
|
||||
homeassistant/components/*/opencv.py
|
||||
|
||||
homeassistant/components/opentherm_gw.py
|
||||
homeassistant/components/*/opentherm_gw.py
|
||||
|
||||
homeassistant/components/openuv/__init__.py
|
||||
homeassistant/components/*/openuv.py
|
||||
|
||||
@ -284,6 +290,9 @@ omit =
|
||||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
homeassistant/components/simplisafe/__init__.py
|
||||
homeassistant/components/*/simplisafe.py
|
||||
|
||||
homeassistant/components/sisyphus.py
|
||||
homeassistant/components/*/sisyphus.py
|
||||
|
||||
@ -379,7 +388,7 @@ omit =
|
||||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/zoneminder/*
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/tuya.py
|
||||
@ -395,7 +404,6 @@ omit =
|
||||
homeassistant/components/alarm_control_panel/ifttt.py
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/alarm_control_panel/totalconnect.py
|
||||
homeassistant/components/alarm_control_panel/yale_smart_alarm.py
|
||||
homeassistant/components/apiai.py
|
||||
@ -426,7 +434,6 @@ omit =
|
||||
homeassistant/components/camera/xeoma.py
|
||||
homeassistant/components/camera/xiaomi.py
|
||||
homeassistant/components/camera/yi.py
|
||||
homeassistant/components/climate/econet.py
|
||||
homeassistant/components/climate/ephember.py
|
||||
homeassistant/components/climate/eq3btsmart.py
|
||||
homeassistant/components/climate/flexit.py
|
||||
@ -434,8 +441,8 @@ omit =
|
||||
homeassistant/components/climate/homematic.py
|
||||
homeassistant/components/climate/honeywell.py
|
||||
homeassistant/components/climate/knx.py
|
||||
homeassistant/components/climate/mill.py
|
||||
homeassistant/components/climate/oem.py
|
||||
homeassistant/components/climate/opentherm_gw.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
homeassistant/components/climate/radiotherm.py
|
||||
homeassistant/components/climate/sensibo.py
|
||||
@ -451,7 +458,6 @@ omit =
|
||||
homeassistant/components/cover/myq.py
|
||||
homeassistant/components/cover/opengarage.py
|
||||
homeassistant/components/cover/rpi_gpio.py
|
||||
homeassistant/components/cover/ryobi_gdo.py
|
||||
homeassistant/components/cover/scsgate.py
|
||||
homeassistant/components/device_tracker/actiontec.py
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
@ -478,6 +484,7 @@ omit =
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
homeassistant/components/device_tracker/nmap_tracker.py
|
||||
homeassistant/components/device_tracker/ping.py
|
||||
homeassistant/components/device_tracker/quantum_gateway.py
|
||||
homeassistant/components/device_tracker/ritassist.py
|
||||
homeassistant/components/device_tracker/sky_hub.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
@ -561,6 +568,7 @@ omit =
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
homeassistant/components/media_player/lg_soundbar.py
|
||||
homeassistant/components/media_player/liveboxplaytv.py
|
||||
homeassistant/components/media_player/mediaroom.py
|
||||
homeassistant/components/media_player/mpchc.py
|
||||
@ -604,6 +612,7 @@ omit =
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/group.py
|
||||
homeassistant/components/notify/hipchat.py
|
||||
homeassistant/components/notify/homematic.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/lannouncer.py
|
||||
@ -636,6 +645,7 @@ omit =
|
||||
homeassistant/components/remember_the_milk/__init__.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/route53.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/airvisual.py
|
||||
@ -747,6 +757,7 @@ omit =
|
||||
homeassistant/components/sensor/radarr.py
|
||||
homeassistant/components/sensor/rainbird.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/rtorrent.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sense.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
@ -776,6 +787,7 @@ omit =
|
||||
homeassistant/components/sensor/tank_utility.py
|
||||
homeassistant/components/sensor/ted5000.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/thermoworks_smoke.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
homeassistant/components/sensor/torque.py
|
||||
homeassistant/components/sensor/trafikverket_weatherstation.py
|
||||
@ -816,6 +828,7 @@ omit =
|
||||
homeassistant/components/switch/pulseaudio_loopback.py
|
||||
homeassistant/components/switch/rainbird.py
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/recswitch.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/snmp.py
|
||||
homeassistant/components/switch/switchbot.py
|
||||
@ -832,6 +845,7 @@ omit =
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/vacuum/mqtt.py
|
||||
homeassistant/components/vacuum/roomba.py
|
||||
homeassistant/components/water_heater/econet.py
|
||||
homeassistant/components/watson_iot.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/buienradar.py
|
||||
|
136
CODEOWNERS
136
CODEOWNERS
@ -2,59 +2,68 @@
|
||||
# when the code that they own is touched.
|
||||
# https://github.com/blog/2392-introducing-code-owners
|
||||
|
||||
# Home Assistant Core
|
||||
setup.py @home-assistant/core
|
||||
homeassistant/*.py @home-assistant/core
|
||||
homeassistant/helpers/* @home-assistant/core
|
||||
homeassistant/util/* @home-assistant/core
|
||||
homeassistant/components/api.py @home-assistant/core
|
||||
homeassistant/components/auth/* @home-assistant/core
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
homeassistant/components/cloud/* @home-assistant/core
|
||||
homeassistant/components/config/* @home-assistant/core
|
||||
homeassistant/components/configurator.py @home-assistant/core
|
||||
homeassistant/components/group.py @home-assistant/core
|
||||
homeassistant/components/conversation/* @home-assistant/core
|
||||
homeassistant/components/frontend/* @home-assistant/core
|
||||
homeassistant/components/group/* @home-assistant/core
|
||||
homeassistant/components/history.py @home-assistant/core
|
||||
homeassistant/components/http/* @home-assistant/core
|
||||
homeassistant/components/input_*.py @home-assistant/core
|
||||
homeassistant/components/introduction.py @home-assistant/core
|
||||
homeassistant/components/logger.py @home-assistant/core
|
||||
homeassistant/components/lovelace/* @home-assistant/core
|
||||
homeassistant/components/mqtt/* @home-assistant/core
|
||||
homeassistant/components/panel_custom.py @home-assistant/core
|
||||
homeassistant/components/panel_iframe.py @home-assistant/core
|
||||
homeassistant/components/persistent_notification.py @home-assistant/core
|
||||
homeassistant/components/onboarding/* @home-assistant/core
|
||||
homeassistant/components/persistent_notification/* @home-assistant/core
|
||||
homeassistant/components/scene/__init__.py @home-assistant/core
|
||||
homeassistant/components/scene/hass.py @home-assistant/core
|
||||
homeassistant/components/script.py @home-assistant/core
|
||||
homeassistant/components/shell_command.py @home-assistant/core
|
||||
homeassistant/components/sun.py @home-assistant/core
|
||||
homeassistant/components/updater.py @home-assistant/core
|
||||
homeassistant/components/weblink.py @home-assistant/core
|
||||
homeassistant/components/weblink/* @home-assistant/core
|
||||
homeassistant/components/websocket_api.py @home-assistant/core
|
||||
homeassistant/components/zone.py @home-assistant/core
|
||||
homeassistant/components/zone/* @home-assistant/core
|
||||
|
||||
# HomeAssistant developer Teams
|
||||
# Home Assistant Developer Teams
|
||||
Dockerfile @home-assistant/docker
|
||||
virtualization/Docker/* @home-assistant/docker
|
||||
|
||||
homeassistant/components/zwave/* @home-assistant/z-wave
|
||||
homeassistant/components/*/zwave.py @home-assistant/z-wave
|
||||
|
||||
homeassistant/components/hassio.py @home-assistant/hassio
|
||||
homeassistant/components/hassio/* @home-assistant/hassio
|
||||
|
||||
# Individual components
|
||||
# Individual platforms
|
||||
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py @bachya
|
||||
homeassistant/components/binary_sensor/hikvision.py @mezz64
|
||||
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
|
||||
homeassistant/components/binary_sensor/threshold.py @fabaff
|
||||
homeassistant/components/camera/yi.py @bachya
|
||||
homeassistant/components/climate/ephember.py @ttroy50
|
||||
homeassistant/components/climate/eq3btsmart.py @rytilahti
|
||||
homeassistant/components/climate/mill.py @danielhiversen
|
||||
homeassistant/components/climate/sensibo.py @andrey-git
|
||||
homeassistant/components/cover/group.py @cdce8p
|
||||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/device_tracker/huawei_router.py @abmantis
|
||||
homeassistant/components/device_tracker/quantum_gateway.py @cisasteelersfan
|
||||
homeassistant/components/device_tracker/tile.py @bachya
|
||||
homeassistant/components/history_graph.py @andrey-git
|
||||
homeassistant/components/light/lifx.py @amelchio
|
||||
homeassistant/components/influx.py @fabaff
|
||||
homeassistant/components/light/lifx_legacy.py @amelchio
|
||||
homeassistant/components/light/tplink.py @rytilahti
|
||||
homeassistant/components/light/yeelight.py @rytilahti
|
||||
@ -65,74 +74,173 @@ homeassistant/components/media_player/kodi.py @armills
|
||||
homeassistant/components/media_player/liveboxplaytv.py @pschmitt
|
||||
homeassistant/components/media_player/mediaroom.py @dgomes
|
||||
homeassistant/components/media_player/monoprice.py @etsinko
|
||||
homeassistant/components/media_player/mpd.py @fabaff
|
||||
homeassistant/components/media_player/sonos.py @amelchio
|
||||
homeassistant/components/media_player/xiaomi_tv.py @fattdev
|
||||
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
|
||||
homeassistant/components/no_ip.py @fabaff
|
||||
homeassistant/components/notify/file.py @fabaff
|
||||
homeassistant/components/notify/flock.py @fabaff
|
||||
homeassistant/components/notify/instapush.py @fabaff
|
||||
homeassistant/components/notify/mastodon.py @fabaff
|
||||
homeassistant/components/notify/smtp.py @fabaff
|
||||
homeassistant/components/notify/syslog.py @fabaff
|
||||
homeassistant/components/notify/xmpp.py @fabaff
|
||||
homeassistant/components/plant.py @ChristianKuehnel
|
||||
homeassistant/components/scene/lifx_cloud.py @amelchio
|
||||
homeassistant/components/sensor/airvisual.py @bachya
|
||||
homeassistant/components/sensor/alpha_vantage.py @fabaff
|
||||
homeassistant/components/sensor/bitcoin.py @fabaff
|
||||
homeassistant/components/sensor/cpuspeed.py @fabaff
|
||||
homeassistant/components/sensor/cups.py @fabaff
|
||||
homeassistant/components/sensor/darksky.py @fabaff
|
||||
homeassistant/components/sensor/file.py @fabaff
|
||||
homeassistant/components/sensor/filter.py @dgomes
|
||||
homeassistant/components/sensor/fixer.py @fabaff
|
||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
homeassistant/components/sensor/gitter.py @fabaff
|
||||
homeassistant/components/sensor/glances.py @fabaff
|
||||
homeassistant/components/sensor/gpsd.py @fabaff
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/jewish_calendar.py @tsvi
|
||||
homeassistant/components/sensor/linux_battery.py @fabaff
|
||||
homeassistant/components/sensor/luftdaten.py @fabaff
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/min_max.py @fabaff
|
||||
homeassistant/components/sensor/moon.py @fabaff
|
||||
homeassistant/components/sensor/netdata.py @fabaff
|
||||
homeassistant/components/sensor/nsw_fuel_station.py @nickw444
|
||||
homeassistant/components/sensor/pi_hole.py @fabaff
|
||||
homeassistant/components/sensor/pollen.py @bachya
|
||||
homeassistant/components/sensor/pvoutput.py @fabaff
|
||||
homeassistant/components/sensor/qnap.py @colinodell
|
||||
homeassistant/components/sensor/scrape.py @fabaff
|
||||
homeassistant/components/sensor/serial.py @fabaff
|
||||
homeassistant/components/sensor/shodan.py @fabaff
|
||||
homeassistant/components/sensor/sma.py @kellerza
|
||||
homeassistant/components/sensor/sql.py @dgomes
|
||||
homeassistant/components/sensor/statistics.py @fabaff
|
||||
homeassistant/components/sensor/swiss*.py @fabaff
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/time_data.py @fabaff
|
||||
homeassistant/components/sensor/version.py @fabaff
|
||||
homeassistant/components/sensor/waqi.py @andrey-git
|
||||
homeassistant/components/sensor/worldclock.py @fabaff
|
||||
homeassistant/components/shiftr.py @fabaff
|
||||
homeassistant/components/spaceapi.py @fabaff
|
||||
homeassistant/components/switch/tplink.py @rytilahti
|
||||
homeassistant/components/vacuum/roomba.py @pschmitt
|
||||
homeassistant/components/weather/__init__.py @fabaff
|
||||
homeassistant/components/weather/darksky.py @fabaff
|
||||
homeassistant/components/weather/demo.py @fabaff
|
||||
homeassistant/components/weather/met.py @danielhiversen
|
||||
homeassistant/components/weather/openweathermap.py @fabaff
|
||||
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
|
||||
# A
|
||||
homeassistant/components/arduino.py @fabaff
|
||||
homeassistant/components/*/arduino.py @fabaff
|
||||
homeassistant/components/*/arest.py @fabaff
|
||||
homeassistant/components/*/axis.py @kane610
|
||||
|
||||
# B
|
||||
homeassistant/components/blink/* @fronzbot
|
||||
homeassistant/components/*/blink.py @fronzbot
|
||||
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
|
||||
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
|
||||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
|
||||
# C
|
||||
homeassistant/components/counter/* @fabaff
|
||||
|
||||
# D
|
||||
homeassistant/components/*/deconz.py @kane610
|
||||
homeassistant/components/digital_ocean.py @fabaff
|
||||
homeassistant/components/*/digital_ocean.py @fabaff
|
||||
homeassistant/components/dweet.py @fabaff
|
||||
homeassistant/components/*/dweet.py @fabaff
|
||||
|
||||
# E
|
||||
homeassistant/components/ecovacs.py @OverloadUT
|
||||
homeassistant/components/*/ecovacs.py @OverloadUT
|
||||
homeassistant/components/edp_redy.py @abmantis
|
||||
homeassistant/components/*/edp_redy.py @abmantis
|
||||
homeassistant/components/edp_redy.py @abmantis
|
||||
homeassistant/components/eight_sleep.py @mezz64
|
||||
homeassistant/components/*/eight_sleep.py @mezz64
|
||||
|
||||
# H
|
||||
homeassistant/components/hive.py @Rendili @KJonline
|
||||
homeassistant/components/*/hive.py @Rendili @KJonline
|
||||
homeassistant/components/homekit/* @cdce8p
|
||||
homeassistant/components/huawei_lte.py @scop
|
||||
homeassistant/components/*/huawei_lte.py @scop
|
||||
|
||||
# K
|
||||
homeassistant/components/knx.py @Julius2342
|
||||
homeassistant/components/*/knx.py @Julius2342
|
||||
homeassistant/components/konnected.py @heythisisnate
|
||||
homeassistant/components/*/konnected.py @heythisisnate
|
||||
|
||||
# L
|
||||
homeassistant/components/lifx.py @amelchio
|
||||
homeassistant/components/*/lifx.py @amelchio
|
||||
|
||||
# M
|
||||
homeassistant/components/matrix.py @tinloaf
|
||||
homeassistant/components/*/matrix.py @tinloaf
|
||||
homeassistant/components/melissa.py @kennedyshead
|
||||
homeassistant/components/*/melissa.py @kennedyshead
|
||||
homeassistant/components/*/mystrom.py @fabaff
|
||||
|
||||
# O
|
||||
homeassistant/components/openuv/* @bachya
|
||||
homeassistant/components/*/openuv.py @bachya
|
||||
|
||||
# Q
|
||||
homeassistant/components/qwikswitch.py @kellerza
|
||||
homeassistant/components/*/qwikswitch.py @kellerza
|
||||
|
||||
# R
|
||||
homeassistant/components/rainmachine/* @bachya
|
||||
homeassistant/components/*/rainmachine.py @bachya
|
||||
homeassistant/components/*/random.py @fabaff
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
|
||||
# S
|
||||
homeassistant/components/simplisafe/* @bachya
|
||||
homeassistant/components/*/simplisafe.py @bachya
|
||||
|
||||
# T
|
||||
homeassistant/components/tahoma.py @philklei
|
||||
homeassistant/components/*/tahoma.py @philklei
|
||||
homeassistant/components/tesla.py @zabuldon
|
||||
homeassistant/components/*/tesla.py @zabuldon
|
||||
homeassistant/components/tellduslive.py @molobrakos @fredrike
|
||||
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
|
||||
homeassistant/components/tesla.py @zabuldon
|
||||
homeassistant/components/*/tesla.py @zabuldon
|
||||
homeassistant/components/thethingsnetwork.py @fabaff
|
||||
homeassistant/components/*/thethingsnetwork.py @fabaff
|
||||
homeassistant/components/tibber/* @danielhiversen
|
||||
homeassistant/components/*/tibber.py @danielhiversen
|
||||
homeassistant/components/tradfri/* @ggravlingen
|
||||
homeassistant/components/*/tradfri.py @ggravlingen
|
||||
|
||||
# U
|
||||
homeassistant/components/unifi.py @kane610
|
||||
homeassistant/components/switch/unifi.py @kane610
|
||||
homeassistant/components/upcloud.py @scop
|
||||
homeassistant/components/*/upcloud.py @scop
|
||||
|
||||
# V
|
||||
homeassistant/components/velux.py @Julius2342
|
||||
homeassistant/components/*/velux.py @Julius2342
|
||||
|
||||
# X
|
||||
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
|
||||
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
|
||||
homeassistant/components/zoneminder.py @rohankapoorcom
|
||||
|
||||
# Z
|
||||
homeassistant/components/zoneminder/ @rohankapoorcom
|
||||
homeassistant/components/*/zoneminder.py @rohankapoorcom
|
||||
|
||||
# Other code
|
||||
homeassistant/scripts/check_config.py @kellerza
|
||||
|
@ -16,6 +16,9 @@ from . import auth_store, models
|
||||
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
|
||||
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
|
||||
|
||||
EVENT_USER_ADDED = 'user_added'
|
||||
EVENT_USER_REMOVED = 'user_removed'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_MfaModuleDict = Dict[str, MultiFactorAuthModule]
|
||||
_ProviderKey = Tuple[str, Optional[str]]
|
||||
@ -126,23 +129,38 @@ class AuthManager:
|
||||
|
||||
async def async_create_system_user(self, name: str) -> models.User:
|
||||
"""Create a system user."""
|
||||
return await self._store.async_create_user(
|
||||
user = await self._store.async_create_user(
|
||||
name=name,
|
||||
system_generated=True,
|
||||
is_active=True,
|
||||
groups=[],
|
||||
)
|
||||
|
||||
self.hass.bus.async_fire(EVENT_USER_ADDED, {
|
||||
'user_id': user.id
|
||||
})
|
||||
|
||||
return user
|
||||
|
||||
async def async_create_user(self, name: str) -> models.User:
|
||||
"""Create a user."""
|
||||
group = (await self._store.async_get_groups())[0]
|
||||
kwargs = {
|
||||
'name': name,
|
||||
'is_active': True,
|
||||
'groups': [group]
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if await self._user_should_be_owner():
|
||||
kwargs['is_owner'] = True
|
||||
|
||||
return await self._store.async_create_user(**kwargs)
|
||||
user = await self._store.async_create_user(**kwargs)
|
||||
|
||||
self.hass.bus.async_fire(EVENT_USER_ADDED, {
|
||||
'user_id': user.id
|
||||
})
|
||||
|
||||
return user
|
||||
|
||||
async def async_get_or_create_user(self, credentials: models.Credentials) \
|
||||
-> models.User:
|
||||
@ -162,12 +180,18 @@ class AuthManager:
|
||||
info = await auth_provider.async_user_meta_for_credentials(
|
||||
credentials)
|
||||
|
||||
return await self._store.async_create_user(
|
||||
user = await self._store.async_create_user(
|
||||
credentials=credentials,
|
||||
name=info.name,
|
||||
is_active=info.is_active,
|
||||
)
|
||||
|
||||
self.hass.bus.async_fire(EVENT_USER_ADDED, {
|
||||
'user_id': user.id
|
||||
})
|
||||
|
||||
return user
|
||||
|
||||
async def async_link_user(self, user: models.User,
|
||||
credentials: models.Credentials) -> None:
|
||||
"""Link credentials to an existing user."""
|
||||
@ -185,6 +209,10 @@ class AuthManager:
|
||||
|
||||
await self._store.async_remove_user(user)
|
||||
|
||||
self.hass.bus.async_fire(EVENT_USER_REMOVED, {
|
||||
'user_id': user.id
|
||||
})
|
||||
|
||||
async def async_activate_user(self, user: models.User) -> None:
|
||||
"""Activate a user."""
|
||||
await self._store.async_activate_user(user)
|
||||
|
@ -10,9 +10,11 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import models
|
||||
from .permissions import DEFAULT_POLICY
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth'
|
||||
INITIAL_GROUP_NAME = 'All Access'
|
||||
|
||||
|
||||
class AuthStore:
|
||||
@ -28,9 +30,18 @@ class AuthStore:
|
||||
"""Initialize the auth store."""
|
||||
self.hass = hass
|
||||
self._users = None # type: Optional[Dict[str, models.User]]
|
||||
self._groups = None # type: Optional[Dict[str, models.Group]]
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
|
||||
private=True)
|
||||
|
||||
async def async_get_groups(self) -> List[models.Group]:
|
||||
"""Retrieve all users."""
|
||||
if self._groups is None:
|
||||
await self._async_load()
|
||||
assert self._groups is not None
|
||||
|
||||
return list(self._groups.values())
|
||||
|
||||
async def async_get_users(self) -> List[models.User]:
|
||||
"""Retrieve all users."""
|
||||
if self._users is None:
|
||||
@ -51,14 +62,20 @@ class AuthStore:
|
||||
self, name: Optional[str], is_owner: Optional[bool] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
system_generated: Optional[bool] = None,
|
||||
credentials: Optional[models.Credentials] = None) -> models.User:
|
||||
credentials: Optional[models.Credentials] = None,
|
||||
groups: Optional[List[models.Group]] = None) -> models.User:
|
||||
"""Create a new user."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
assert self._users is not None
|
||||
|
||||
assert self._users is not None
|
||||
assert self._groups is not None
|
||||
|
||||
kwargs = {
|
||||
'name': name
|
||||
'name': name,
|
||||
# Until we get group management, we just put everyone in the
|
||||
# same group.
|
||||
'groups': groups or [],
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if is_owner is not None:
|
||||
@ -219,19 +236,40 @@ class AuthStore:
|
||||
return
|
||||
|
||||
users = OrderedDict() # type: Dict[str, models.User]
|
||||
groups = OrderedDict() # type: Dict[str, models.Group]
|
||||
|
||||
# When creating objects we mention each attribute explicetely. This
|
||||
# prevents crashing if user rolls back HA version after a new property
|
||||
# was added.
|
||||
|
||||
for group_dict in data.get('groups', []):
|
||||
groups[group_dict['id']] = models.Group(
|
||||
name=group_dict['name'],
|
||||
id=group_dict['id'],
|
||||
policy=group_dict.get('policy', DEFAULT_POLICY),
|
||||
)
|
||||
|
||||
migrate_group = None
|
||||
|
||||
if not groups:
|
||||
migrate_group = models.Group(
|
||||
name=INITIAL_GROUP_NAME,
|
||||
policy=DEFAULT_POLICY
|
||||
)
|
||||
groups[migrate_group.id] = migrate_group
|
||||
|
||||
for user_dict in data['users']:
|
||||
users[user_dict['id']] = models.User(
|
||||
name=user_dict['name'],
|
||||
groups=[groups[group_id] for group_id
|
||||
in user_dict.get('group_ids', [])],
|
||||
id=user_dict['id'],
|
||||
is_owner=user_dict['is_owner'],
|
||||
is_active=user_dict['is_active'],
|
||||
system_generated=user_dict['system_generated'],
|
||||
)
|
||||
if migrate_group is not None and not user_dict['system_generated']:
|
||||
users[user_dict['id']].groups = [migrate_group]
|
||||
|
||||
for cred_dict in data['credentials']:
|
||||
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
||||
@ -286,6 +324,7 @@ class AuthStore:
|
||||
)
|
||||
users[rt_dict['user_id']].refresh_tokens[token.id] = token
|
||||
|
||||
self._groups = groups
|
||||
self._users = users
|
||||
|
||||
@callback
|
||||
@ -300,10 +339,12 @@ class AuthStore:
|
||||
def _data_to_save(self) -> Dict:
|
||||
"""Return the data to store."""
|
||||
assert self._users is not None
|
||||
assert self._groups is not None
|
||||
|
||||
users = [
|
||||
{
|
||||
'id': user.id,
|
||||
'group_ids': [group.id for group in user.groups],
|
||||
'is_owner': user.is_owner,
|
||||
'is_active': user.is_active,
|
||||
'name': user.name,
|
||||
@ -312,6 +353,18 @@ class AuthStore:
|
||||
for user in self._users.values()
|
||||
]
|
||||
|
||||
groups = []
|
||||
for group in self._groups.values():
|
||||
g_dict = {
|
||||
'name': group.name,
|
||||
'id': group.id,
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if group.policy is not DEFAULT_POLICY:
|
||||
g_dict['policy'] = group.policy
|
||||
|
||||
groups.append(g_dict)
|
||||
|
||||
credentials = [
|
||||
{
|
||||
'id': credential.id,
|
||||
@ -348,6 +401,7 @@ class AuthStore:
|
||||
|
||||
return {
|
||||
'users': users,
|
||||
'groups': groups,
|
||||
'credentials': credentials,
|
||||
'refresh_tokens': refresh_tokens,
|
||||
}
|
||||
@ -355,3 +409,14 @@ class AuthStore:
|
||||
def _set_defaults(self) -> None:
|
||||
"""Set default values for auth store."""
|
||||
self._users = OrderedDict() # type: Dict[str, models.User]
|
||||
|
||||
# Add default group
|
||||
all_access_group = models.Group(
|
||||
name=INITIAL_GROUP_NAME,
|
||||
policy=DEFAULT_POLICY,
|
||||
)
|
||||
|
||||
groups = OrderedDict() # type: Dict[str, models.Group]
|
||||
groups[all_access_group.id] = all_access_group
|
||||
|
||||
self._groups = groups
|
||||
|
@ -7,6 +7,7 @@ import attr
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import permissions as perm_mdl
|
||||
from .util import generate_secret
|
||||
|
||||
TOKEN_TYPE_NORMAL = 'normal'
|
||||
@ -14,6 +15,15 @@ TOKEN_TYPE_SYSTEM = 'system'
|
||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token'
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class Group:
|
||||
"""A group."""
|
||||
|
||||
name = attr.ib(type=str) # type: Optional[str]
|
||||
policy = attr.ib(type=perm_mdl.PolicyType)
|
||||
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class User:
|
||||
"""A user."""
|
||||
@ -24,6 +34,8 @@ class User:
|
||||
is_active = attr.ib(type=bool, default=False)
|
||||
system_generated = attr.ib(type=bool, default=False)
|
||||
|
||||
groups = attr.ib(type=List, factory=list, cmp=False) # type: List[Group]
|
||||
|
||||
# List of credentials of a user.
|
||||
credentials = attr.ib(
|
||||
type=list, factory=list, cmp=False
|
||||
@ -34,6 +46,28 @@ class User:
|
||||
type=dict, factory=dict, cmp=False
|
||||
) # type: Dict[str, RefreshToken]
|
||||
|
||||
_permissions = attr.ib(
|
||||
type=perm_mdl.PolicyPermissions,
|
||||
init=False,
|
||||
cmp=False,
|
||||
default=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def permissions(self) -> perm_mdl.AbstractPermissions:
|
||||
"""Return permissions object for user."""
|
||||
if self.is_owner:
|
||||
return perm_mdl.OwnerPermissions
|
||||
|
||||
if self._permissions is not None:
|
||||
return self._permissions
|
||||
|
||||
self._permissions = perm_mdl.PolicyPermissions(
|
||||
perm_mdl.merge_policies([
|
||||
group.policy for group in self.groups]))
|
||||
|
||||
return self._permissions
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class RefreshToken:
|
||||
|
252
homeassistant/auth/permissions.py
Normal file
252
homeassistant/auth/permissions.py
Normal file
@ -0,0 +1,252 @@
|
||||
"""Permissions for Home Assistant."""
|
||||
from typing import ( # noqa: F401
|
||||
cast, Any, Callable, Dict, List, Mapping, Set, Tuple, Union)
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import State
|
||||
|
||||
CategoryType = Union[Mapping[str, 'CategoryType'], bool, None]
|
||||
PolicyType = Mapping[str, CategoryType]
|
||||
|
||||
|
||||
# Default policy if group has no policy applied.
|
||||
DEFAULT_POLICY = {
|
||||
"entities": True
|
||||
} # type: PolicyType
|
||||
|
||||
CAT_ENTITIES = 'entities'
|
||||
ENTITY_DOMAINS = 'domains'
|
||||
ENTITY_ENTITY_IDS = 'entity_ids'
|
||||
|
||||
VALUES_SCHEMA = vol.Any(True, vol.Schema({
|
||||
str: True
|
||||
}))
|
||||
|
||||
ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({
|
||||
vol.Optional(ENTITY_DOMAINS): VALUES_SCHEMA,
|
||||
vol.Optional(ENTITY_ENTITY_IDS): VALUES_SCHEMA,
|
||||
}))
|
||||
|
||||
POLICY_SCHEMA = vol.Schema({
|
||||
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
|
||||
})
|
||||
|
||||
|
||||
class AbstractPermissions:
|
||||
"""Default permissions class."""
|
||||
|
||||
def check_entity(self, entity_id: str, *keys: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
raise NotImplementedError
|
||||
|
||||
def filter_states(self, states: List[State]) -> List[State]:
|
||||
"""Filter a list of states for what the user is allowed to see."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PolicyPermissions(AbstractPermissions):
|
||||
"""Handle permissions."""
|
||||
|
||||
def __init__(self, policy: PolicyType) -> None:
|
||||
"""Initialize the permission class."""
|
||||
self._policy = policy
|
||||
self._compiled = {} # type: Dict[str, Callable[..., bool]]
|
||||
|
||||
def check_entity(self, entity_id: str, *keys: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
func = self._policy_func(CAT_ENTITIES, _compile_entities)
|
||||
return func(entity_id, keys)
|
||||
|
||||
def filter_states(self, states: List[State]) -> List[State]:
|
||||
"""Filter a list of states for what the user is allowed to see."""
|
||||
func = self._policy_func(CAT_ENTITIES, _compile_entities)
|
||||
keys = ('read',)
|
||||
return [entity for entity in states if func(entity.entity_id, keys)]
|
||||
|
||||
def _policy_func(self, category: str,
|
||||
compile_func: Callable[[CategoryType], Callable]) \
|
||||
-> Callable[..., bool]:
|
||||
"""Get a policy function."""
|
||||
func = self._compiled.get(category)
|
||||
|
||||
if func:
|
||||
return func
|
||||
|
||||
func = self._compiled[category] = compile_func(
|
||||
self._policy.get(category))
|
||||
return func
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Equals check."""
|
||||
# pylint: disable=protected-access
|
||||
return (isinstance(other, PolicyPermissions) and
|
||||
other._policy == self._policy)
|
||||
|
||||
|
||||
class _OwnerPermissions(AbstractPermissions):
|
||||
"""Owner permissions."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
def check_entity(self, entity_id: str, *keys: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
return True
|
||||
|
||||
def filter_states(self, states: List[State]) -> List[State]:
|
||||
"""Filter a list of states for what the user is allowed to see."""
|
||||
return states
|
||||
|
||||
|
||||
OwnerPermissions = _OwnerPermissions() # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def _compile_entities(policy: CategoryType) \
|
||||
-> Callable[[str, Tuple[str]], bool]:
|
||||
"""Compile policy into a function that tests policy."""
|
||||
# None, Empty Dict, False
|
||||
if not policy:
|
||||
def apply_policy_deny_all(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Decline all."""
|
||||
return False
|
||||
|
||||
return apply_policy_deny_all
|
||||
|
||||
if policy is True:
|
||||
def apply_policy_allow_all(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Approve all."""
|
||||
return True
|
||||
|
||||
return apply_policy_allow_all
|
||||
|
||||
assert isinstance(policy, dict)
|
||||
|
||||
domains = policy.get(ENTITY_DOMAINS)
|
||||
entity_ids = policy.get(ENTITY_ENTITY_IDS)
|
||||
|
||||
funcs = [] # type: List[Callable[[str, Tuple[str]], Union[None, bool]]]
|
||||
|
||||
# The order of these functions matter. The more precise are at the top.
|
||||
# If a function returns None, they cannot handle it.
|
||||
# If a function returns a boolean, that's the result to return.
|
||||
|
||||
# Setting entity_ids to a boolean is final decision for permissions
|
||||
# So return right away.
|
||||
if isinstance(entity_ids, bool):
|
||||
def apply_entity_id_policy(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Test if allowed entity_id."""
|
||||
return entity_ids # type: ignore
|
||||
|
||||
return apply_entity_id_policy
|
||||
|
||||
if entity_ids is not None:
|
||||
def allowed_entity_id(entity_id: str, keys: Tuple[str]) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed entity_id."""
|
||||
return entity_ids.get(entity_id) # type: ignore
|
||||
|
||||
funcs.append(allowed_entity_id)
|
||||
|
||||
if isinstance(domains, bool):
|
||||
def allowed_domain(entity_id: str, keys: Tuple[str]) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed domain."""
|
||||
return domains
|
||||
|
||||
funcs.append(allowed_domain)
|
||||
|
||||
elif domains is not None:
|
||||
def allowed_domain(entity_id: str, keys: Tuple[str]) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed domain."""
|
||||
domain = entity_id.split(".", 1)[0]
|
||||
return domains.get(domain) # type: ignore
|
||||
|
||||
funcs.append(allowed_domain)
|
||||
|
||||
# Can happen if no valid subcategories specified
|
||||
if not funcs:
|
||||
def apply_policy_deny_all_2(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Decline all."""
|
||||
return False
|
||||
|
||||
return apply_policy_deny_all_2
|
||||
|
||||
if len(funcs) == 1:
|
||||
func = funcs[0]
|
||||
|
||||
def apply_policy_func(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Apply a single policy function."""
|
||||
return func(entity_id, keys) is True
|
||||
|
||||
return apply_policy_func
|
||||
|
||||
def apply_policy_funcs(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Apply several policy functions."""
|
||||
for func in funcs:
|
||||
result = func(entity_id, keys)
|
||||
if result is not None:
|
||||
return result
|
||||
return False
|
||||
|
||||
return apply_policy_funcs
|
||||
|
||||
|
||||
def merge_policies(policies: List[PolicyType]) -> PolicyType:
|
||||
"""Merge policies."""
|
||||
new_policy = {} # type: Dict[str, CategoryType]
|
||||
seen = set() # type: Set[str]
|
||||
for policy in policies:
|
||||
for category in policy:
|
||||
if category in seen:
|
||||
continue
|
||||
seen.add(category)
|
||||
new_policy[category] = _merge_policies([
|
||||
policy.get(category) for policy in policies])
|
||||
cast(PolicyType, new_policy)
|
||||
return new_policy
|
||||
|
||||
|
||||
def _merge_policies(sources: List[CategoryType]) -> CategoryType:
|
||||
"""Merge a policy."""
|
||||
# When merging policies, the most permissive wins.
|
||||
# This means we order it like this:
|
||||
# True > Dict > None
|
||||
#
|
||||
# True: allow everything
|
||||
# Dict: specify more granular permissions
|
||||
# None: no opinion
|
||||
#
|
||||
# If there are multiple sources with a dict as policy, we recursively
|
||||
# merge each key in the source.
|
||||
|
||||
policy = None # type: CategoryType
|
||||
seen = set() # type: Set[str]
|
||||
for source in sources:
|
||||
if source is None:
|
||||
continue
|
||||
|
||||
# A source that's True will always win. Shortcut return.
|
||||
if source is True:
|
||||
return True
|
||||
|
||||
assert isinstance(source, dict)
|
||||
|
||||
if policy is None:
|
||||
policy = {}
|
||||
|
||||
assert isinstance(policy, dict)
|
||||
|
||||
for key in source:
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
|
||||
key_sources = []
|
||||
for src in sources:
|
||||
if isinstance(src, dict):
|
||||
key_sources.append(src.get(key))
|
||||
|
||||
policy[key] = _merge_policies(key_sources)
|
||||
|
||||
return policy
|
@ -12,6 +12,8 @@ import itertools as it
|
||||
import logging
|
||||
from typing import Awaitable
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.config as conf_util
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@ -21,11 +23,16 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
||||
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
|
||||
RESTART_EXIT_CODE)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
|
||||
SERVICE_CHECK_CONFIG = 'check_config'
|
||||
SERVICE_UPDATE_ENTITY = 'update_entity'
|
||||
SCHEMA_UPDATE_ENTITY = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_id
|
||||
})
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
@ -133,12 +140,20 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||
if call.service == SERVICE_HOMEASSISTANT_RESTART:
|
||||
hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE))
|
||||
|
||||
async def async_handle_update_service(call):
|
||||
"""Service handler for updating an entity."""
|
||||
await hass.helpers.entity_component.async_update_entity(
|
||||
call.data[ATTR_ENTITY_ID])
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_UPDATE_ENTITY, async_handle_update_service,
|
||||
schema=SCHEMA_UPDATE_ENTITY)
|
||||
|
||||
async def async_handle_reload_config(call):
|
||||
"""Service handler for reloading core config."""
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['abodepy==0.13.1']
|
||||
REQUIREMENTS = ['abodepy==0.14.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
204
homeassistant/components/alarm_control_panel/elkm1.py
Normal file
204
homeassistant/components/alarm_control_panel/elkm1.py
Normal file
@ -0,0 +1,204 @@
|
||||
"""
|
||||
Each ElkM1 area will be created as a separate alarm_control_panel in HASS.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.elkm1/
|
||||
"""
|
||||
|
||||
import voluptuous as vol
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMING, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED)
|
||||
from homeassistant.components.elkm1 import (
|
||||
DOMAIN as ELK_DOMAIN, create_elk_entities, ElkEntity)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect, async_dispatcher_send)
|
||||
|
||||
DEPENDENCIES = [ELK_DOMAIN]
|
||||
|
||||
SIGNAL_ARM_ENTITY = 'elkm1_arm'
|
||||
SIGNAL_DISPLAY_MESSAGE = 'elkm1_display_message'
|
||||
|
||||
ELK_ALARM_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID, default=[]): cv.entity_ids,
|
||||
vol.Required(ATTR_CODE): vol.All(vol.Coerce(int), vol.Range(0, 999999)),
|
||||
})
|
||||
|
||||
DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids,
|
||||
vol.Optional('clear', default=2): vol.In([0, 1, 2]),
|
||||
vol.Optional('beep', default=False): cv.boolean,
|
||||
vol.Optional('timeout', default=0): vol.Range(min=0, max=65535),
|
||||
vol.Optional('line1', default=''): cv.string,
|
||||
vol.Optional('line2', default=''): cv.string,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the ElkM1 alarm platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
elk = hass.data[ELK_DOMAIN]['elk']
|
||||
entities = create_elk_entities(hass, elk.areas, 'area', ElkArea, [])
|
||||
async_add_entities(entities, True)
|
||||
|
||||
def _dispatch(signal, entity_ids, *args):
|
||||
for entity_id in entity_ids:
|
||||
async_dispatcher_send(
|
||||
hass, '{}_{}'.format(signal, entity_id), *args)
|
||||
|
||||
def _arm_service(service):
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID, [])
|
||||
arm_level = _arm_services().get(service.service)
|
||||
args = (arm_level, service.data.get(ATTR_CODE))
|
||||
_dispatch(SIGNAL_ARM_ENTITY, entity_ids, *args)
|
||||
|
||||
for service in _arm_services():
|
||||
hass.services.async_register(
|
||||
alarm.DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA)
|
||||
|
||||
def _display_message_service(service):
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID, [])
|
||||
data = service.data
|
||||
args = (data['clear'], data['beep'], data['timeout'],
|
||||
data['line1'], data['line2'])
|
||||
_dispatch(SIGNAL_DISPLAY_MESSAGE, entity_ids, *args)
|
||||
|
||||
hass.services.async_register(
|
||||
alarm.DOMAIN, 'elkm1_alarm_display_message',
|
||||
_display_message_service, DISPLAY_MESSAGE_SERVICE_SCHEMA)
|
||||
|
||||
|
||||
def _arm_services():
|
||||
from elkm1_lib.const import ArmLevel
|
||||
|
||||
return {
|
||||
'elkm1_alarm_arm_vacation': ArmLevel.ARMED_VACATION.value,
|
||||
'elkm1_alarm_arm_home_instant': ArmLevel.ARMED_STAY_INSTANT.value,
|
||||
'elkm1_alarm_arm_night_instant': ArmLevel.ARMED_NIGHT_INSTANT.value,
|
||||
}
|
||||
|
||||
|
||||
class ElkArea(ElkEntity, alarm.AlarmControlPanel):
|
||||
"""Representation of an Area / Partition within the ElkM1 alarm panel."""
|
||||
|
||||
def __init__(self, element, elk, elk_data):
|
||||
"""Initialize Area as Alarm Control Panel."""
|
||||
super().__init__(element, elk, elk_data)
|
||||
self._changed_by_entity_id = ''
|
||||
self._state = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callback for ElkM1 changes."""
|
||||
await super().async_added_to_hass()
|
||||
for keypad in self._elk.keypads:
|
||||
keypad.add_callback(self._watch_keypad)
|
||||
async_dispatcher_connect(
|
||||
self.hass, '{}_{}'.format(SIGNAL_ARM_ENTITY, self.entity_id),
|
||||
self._arm_service)
|
||||
async_dispatcher_connect(
|
||||
self.hass, '{}_{}'.format(SIGNAL_DISPLAY_MESSAGE, self.entity_id),
|
||||
self._display_message)
|
||||
|
||||
def _watch_keypad(self, keypad, changeset):
|
||||
if keypad.area != self._element.index:
|
||||
return
|
||||
if changeset.get('last_user') is not None:
|
||||
self._changed_by_entity_id = self.hass.data[
|
||||
ELK_DOMAIN]['keypads'].get(keypad.index, '')
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the alarm code format."""
|
||||
return '^[0-9]{4}([0-9]{2})?$'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the element."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Attributes of the area."""
|
||||
from elkm1_lib.const import AlarmState, ArmedStatus, ArmUpState
|
||||
|
||||
attrs = self.initial_attrs()
|
||||
elmt = self._element
|
||||
attrs['is_exit'] = elmt.is_exit
|
||||
attrs['timer1'] = elmt.timer1
|
||||
attrs['timer2'] = elmt.timer2
|
||||
if elmt.armed_status is not None:
|
||||
attrs['armed_status'] = \
|
||||
ArmedStatus(elmt.armed_status).name.lower()
|
||||
if elmt.arm_up_state is not None:
|
||||
attrs['arm_up_state'] = ArmUpState(elmt.arm_up_state).name.lower()
|
||||
if elmt.alarm_state is not None:
|
||||
attrs['alarm_state'] = AlarmState(elmt.alarm_state).name.lower()
|
||||
attrs['changed_by_entity_id'] = self._changed_by_entity_id
|
||||
return attrs
|
||||
|
||||
def _element_changed(self, element, changeset):
|
||||
from elkm1_lib.const import ArmedStatus
|
||||
|
||||
elk_state_to_hass_state = {
|
||||
ArmedStatus.DISARMED.value: STATE_ALARM_DISARMED,
|
||||
ArmedStatus.ARMED_AWAY.value: STATE_ALARM_ARMED_AWAY,
|
||||
ArmedStatus.ARMED_STAY.value: STATE_ALARM_ARMED_HOME,
|
||||
ArmedStatus.ARMED_STAY_INSTANT.value: STATE_ALARM_ARMED_HOME,
|
||||
ArmedStatus.ARMED_TO_NIGHT.value: STATE_ALARM_ARMED_NIGHT,
|
||||
ArmedStatus.ARMED_TO_NIGHT_INSTANT.value: STATE_ALARM_ARMED_NIGHT,
|
||||
ArmedStatus.ARMED_TO_VACATION.value: STATE_ALARM_ARMED_AWAY,
|
||||
}
|
||||
|
||||
if self._element.alarm_state is None:
|
||||
self._state = None
|
||||
elif self._area_is_in_alarm_state():
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
elif self._entry_exit_timer_is_running():
|
||||
self._state = STATE_ALARM_ARMING \
|
||||
if self._element.is_exit else STATE_ALARM_PENDING
|
||||
else:
|
||||
self._state = elk_state_to_hass_state[self._element.armed_status]
|
||||
|
||||
def _entry_exit_timer_is_running(self):
|
||||
return self._element.timer1 > 0 or self._element.timer2 > 0
|
||||
|
||||
def _area_is_in_alarm_state(self):
|
||||
from elkm1_lib.const import AlarmState
|
||||
|
||||
return self._element.alarm_state >= AlarmState.FIRE_ALARM.value
|
||||
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._element.disarm(int(code))
|
||||
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
from elkm1_lib.const import ArmLevel
|
||||
|
||||
self._element.arm(ArmLevel.ARMED_STAY.value, int(code))
|
||||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
from elkm1_lib.const import ArmLevel
|
||||
|
||||
self._element.arm(ArmLevel.ARMED_AWAY.value, int(code))
|
||||
|
||||
async def async_alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
from elkm1_lib.const import ArmLevel
|
||||
|
||||
self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code))
|
||||
|
||||
async def _arm_service(self, arm_level, code):
|
||||
self._element.arm(arm_level, code)
|
||||
|
||||
async def _display_message(self, clear, beep, timeout, line1, line2):
|
||||
"""Display a message on all keypads for the area."""
|
||||
self._element.display_message(clear, beep, timeout, line1, line2)
|
@ -79,3 +79,55 @@ ifttt_push_alarm_state:
|
||||
state:
|
||||
description: The state to which the alarm control panel has to be set.
|
||||
example: 'armed_night'
|
||||
|
||||
elkm1_alarm_arm_vacation:
|
||||
description: Arm the ElkM1 in vacation mode.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm.
|
||||
example: 'alarm_control_panel.main'
|
||||
code:
|
||||
description: An code to arm the alarm control panel.
|
||||
example: 1234
|
||||
|
||||
elkm1_alarm_arm_home_instant:
|
||||
description: Arm the ElkM1 in home instant mode.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm.
|
||||
example: 'alarm_control_panel.main'
|
||||
code:
|
||||
description: An code to arm the alarm control panel.
|
||||
example: 1234
|
||||
|
||||
elkm1_alarm_arm_night_instant:
|
||||
description: Arm the ElkM1 in night instant mode.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm.
|
||||
example: 'alarm_control_panel.main'
|
||||
code:
|
||||
description: An code to arm the alarm control panel.
|
||||
example: 1234
|
||||
|
||||
elkm1_alarm_display_message:
|
||||
description: Display a message on all of the ElkM1 keypads for an area.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to display messages on.
|
||||
example: 'alarm_control_panel.main'
|
||||
clear:
|
||||
description: 0=clear message, 1=clear message with * key, 2=Display until timeout; default 2
|
||||
example: 1
|
||||
beep:
|
||||
description: 0=no beep, 1=beep; default 0
|
||||
example: 1
|
||||
timeout:
|
||||
description: Time to display message, 0=forever, max 65535, default 0
|
||||
example: 4242
|
||||
line1:
|
||||
description: Up to 16 characters of text (truncated if too long). Default blank.
|
||||
example: The answer to life,
|
||||
line2:
|
||||
description: Up to 16 characters of text (truncated if too long). Default blank.
|
||||
example: the universe, and everything.
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
Interfaces with SimpliSafe alarm control panel.
|
||||
This platform provides alarm control functionality for SimpliSafe.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.simplisafe/
|
||||
@ -7,86 +7,44 @@ https://home-assistant.io/components/alarm_control_panel.simplisafe/
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
PLATFORM_SCHEMA, AlarmControlPanel)
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.simplisafe.const import (
|
||||
DATA_CLIENT, DOMAIN, TOPIC_UPDATE)
|
||||
from homeassistant.const import (
|
||||
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
REQUIREMENTS = ['simplisafe-python==3.1.2']
|
||||
CONF_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_ALARM_ACTIVE = "alarm_active"
|
||||
ATTR_TEMPERATURE = "temperature"
|
||||
|
||||
DATA_FILE = '.simplisafe'
|
||||
|
||||
DEFAULT_NAME = 'SimpliSafe'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
})
|
||||
ATTR_ALARM_ACTIVE = 'alarm_active'
|
||||
ATTR_TEMPERATURE = 'temperature'
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the SimpliSafe platform."""
|
||||
from simplipy import API
|
||||
from simplipy.errors import SimplipyError
|
||||
"""Set up a SimpliSafe alarm control panel based on existing config."""
|
||||
pass
|
||||
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
|
||||
config_data = await hass.async_add_executor_job(
|
||||
load_json, hass.config.path(DATA_FILE))
|
||||
|
||||
try:
|
||||
if config_data:
|
||||
try:
|
||||
simplisafe = await API.login_via_token(
|
||||
config_data['refresh_token'], websession)
|
||||
_LOGGER.debug('Logging in with refresh token')
|
||||
except SimplipyError:
|
||||
_LOGGER.info('Refresh token expired; attempting credentials')
|
||||
simplisafe = await API.login_via_credentials(
|
||||
username, password, websession)
|
||||
else:
|
||||
simplisafe = await API.login_via_credentials(
|
||||
username, password, websession)
|
||||
_LOGGER.debug('Logging in with credentials')
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error("There was an error during setup: %s", err)
|
||||
return
|
||||
|
||||
config_data = {'refresh_token': simplisafe.refresh_token}
|
||||
await hass.async_add_executor_job(
|
||||
save_json, hass.config.path(DATA_FILE), config_data)
|
||||
|
||||
systems = await simplisafe.get_systems()
|
||||
async_add_entities(
|
||||
[SimpliSafeAlarm(system, name, code) for system in systems], True)
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up a SimpliSafe alarm control panel based on a config entry."""
|
||||
systems = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||
async_add_entities([
|
||||
SimpliSafeAlarm(system, entry.data.get(CONF_CODE))
|
||||
for system in systems
|
||||
], True)
|
||||
|
||||
|
||||
class SimpliSafeAlarm(AlarmControlPanel):
|
||||
"""Representation of a SimpliSafe alarm."""
|
||||
|
||||
def __init__(self, system, name, code):
|
||||
def __init__(self, system, code):
|
||||
"""Initialize the SimpliSafe alarm."""
|
||||
self._async_unsub_dispatcher_connect = None
|
||||
self._attrs = {}
|
||||
self._code = str(code) if code else None
|
||||
self._name = name
|
||||
self._code = code
|
||||
self._system = system
|
||||
self._state = None
|
||||
|
||||
@ -98,9 +56,7 @@ class SimpliSafeAlarm(AlarmControlPanel):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
if self._name:
|
||||
return self._name
|
||||
return 'Alarm {}'.format(self._system.system_id)
|
||||
return self._system.address
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
@ -128,6 +84,21 @@ class SimpliSafeAlarm(AlarmControlPanel):
|
||||
_LOGGER.warning("Wrong code entered for %s", state)
|
||||
return check
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@callback
|
||||
def update():
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
|
||||
self.hass, TOPIC_UPDATE, update)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect dispatcher listener when removed."""
|
||||
if self._async_unsub_dispatcher_connect:
|
||||
self._async_unsub_dispatcher_connect()
|
||||
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, 'disarming'):
|
||||
@ -151,22 +122,24 @@ class SimpliSafeAlarm(AlarmControlPanel):
|
||||
|
||||
async def async_update(self):
|
||||
"""Update alarm status."""
|
||||
await self._system.update()
|
||||
from simplipy.system import SystemStates
|
||||
|
||||
if self._system.state == self._system.SystemStates.off:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif self._system.state in (
|
||||
self._system.SystemStates.home,
|
||||
self._system.SystemStates.home_count):
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif self._system.state in (
|
||||
self._system.SystemStates.away,
|
||||
self._system.SystemStates.away_count,
|
||||
self._system.SystemStates.exit_delay):
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
self._state = None
|
||||
await self._system.update()
|
||||
|
||||
self._attrs[ATTR_ALARM_ACTIVE] = self._system.alarm_going_off
|
||||
if self._system.temperature:
|
||||
self._attrs[ATTR_TEMPERATURE] = self._system.temperature
|
||||
|
||||
if self._system.state == SystemStates.error:
|
||||
return
|
||||
|
||||
if self._system.state == SystemStates.off:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif self._system.state in (SystemStates.home,
|
||||
SystemStates.home_count):
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif self._system.state in (SystemStates.away, SystemStates.away_count,
|
||||
SystemStates.exit_delay):
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
self._state = None
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.18']
|
||||
REQUIREMENTS = ['total_connect_client==0.20']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -49,10 +49,9 @@ def setup(hass, config):
|
||||
|
||||
# It doesn't really matter why we're not able to get the status, just that
|
||||
# we can't.
|
||||
# pylint: disable=broad-except
|
||||
try:
|
||||
DATA.update(no_throttle=True)
|
||||
except Exception:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Failure while testing APCUPSd status retrieval.")
|
||||
return False
|
||||
return True
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.2.0']
|
||||
REQUIREMENTS = ['pyarlo==0.2.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -81,7 +81,7 @@ def setup(hass, config):
|
||||
|
||||
def hub_refresh(event_time):
|
||||
"""Call ArloHub to refresh information."""
|
||||
_LOGGER.info("Updating Arlo Hub component")
|
||||
_LOGGER.debug("Updating Arlo Hub component")
|
||||
hass.data[DATA_ARLO].update(update_cameras=True,
|
||||
update_base_station=True)
|
||||
dispatcher_send(hass, SIGNAL_UPDATE_ARLO)
|
||||
|
@ -31,7 +31,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_PORT): int,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
@ -4,7 +4,6 @@ Support for August devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/august/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
@ -124,6 +123,7 @@ def setup_august(hass, config, api, authenticator):
|
||||
|
||||
return True
|
||||
if state == AuthenticationState.BAD_PASSWORD:
|
||||
_LOGGER.error("Invalid password provided")
|
||||
return False
|
||||
if state == AuthenticationState.REQUIRES_VALIDATION:
|
||||
request_configuration(hass, config, api, authenticator)
|
||||
@ -165,6 +165,7 @@ class AugustData:
|
||||
self._doorbell_detail_by_id = {}
|
||||
self._lock_status_by_id = {}
|
||||
self._lock_detail_by_id = {}
|
||||
self._door_state_by_id = {}
|
||||
self._activities_by_id = {}
|
||||
|
||||
@property
|
||||
@ -184,6 +185,7 @@ class AugustData:
|
||||
|
||||
def get_device_activities(self, device_id, *activity_types):
|
||||
"""Return a list of activities."""
|
||||
_LOGGER.debug("Getting device activities")
|
||||
self._update_device_activities()
|
||||
|
||||
activities = self._activities_by_id.get(device_id, [])
|
||||
@ -199,6 +201,7 @@ class AugustData:
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
||||
"""Update data object with latest from August API."""
|
||||
_LOGGER.debug("Updating device activities")
|
||||
for house_id in self.house_ids:
|
||||
activities = self._api.get_house_activities(self._access_token,
|
||||
house_id,
|
||||
@ -218,14 +221,21 @@ class AugustData:
|
||||
def _update_doorbells(self):
|
||||
detail_by_id = {}
|
||||
|
||||
_LOGGER.debug("Start retrieving doorbell details")
|
||||
for doorbell in self._doorbells:
|
||||
_LOGGER.debug("Updating status for %s",
|
||||
doorbell.device_name)
|
||||
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
|
||||
self._access_token, doorbell.device_id)
|
||||
|
||||
_LOGGER.debug("Completed retrieving doorbell details")
|
||||
self._doorbell_detail_by_id = detail_by_id
|
||||
|
||||
def get_lock_status(self, lock_id):
|
||||
"""Return lock status."""
|
||||
"""Return status if the door is locked or unlocked.
|
||||
|
||||
This is status for the lock itself.
|
||||
"""
|
||||
self._update_locks()
|
||||
return self._lock_status_by_id.get(lock_id)
|
||||
|
||||
@ -234,17 +244,43 @@ class AugustData:
|
||||
self._update_locks()
|
||||
return self._lock_detail_by_id.get(lock_id)
|
||||
|
||||
def get_door_state(self, lock_id):
|
||||
"""Return status if the door is open or closed.
|
||||
|
||||
This is the status from the door sensor.
|
||||
"""
|
||||
self._update_doors()
|
||||
return self._door_state_by_id.get(lock_id)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _update_doors(self):
|
||||
state_by_id = {}
|
||||
|
||||
_LOGGER.debug("Start retrieving door status")
|
||||
for lock in self._locks:
|
||||
_LOGGER.debug("Updating status for %s",
|
||||
lock.device_name)
|
||||
state_by_id[lock.device_id] = self._api.get_lock_door_status(
|
||||
self._access_token, lock.device_id)
|
||||
|
||||
_LOGGER.debug("Completed retrieving door status")
|
||||
self._door_state_by_id = state_by_id
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _update_locks(self):
|
||||
status_by_id = {}
|
||||
detail_by_id = {}
|
||||
|
||||
_LOGGER.debug("Start retrieving locks status")
|
||||
for lock in self._locks:
|
||||
_LOGGER.debug("Updating status for %s",
|
||||
lock.device_name)
|
||||
status_by_id[lock.device_id] = self._api.get_lock_status(
|
||||
self._access_token, lock.device_id)
|
||||
detail_by_id[lock.device_id] = self._api.get_lock_detail(
|
||||
self._access_token, lock.device_id)
|
||||
|
||||
_LOGGER.debug("Completed retrieving locks status")
|
||||
self._lock_status_by_id = status_by_id
|
||||
self._lock_detail_by_id = detail_by_id
|
||||
|
||||
|
34
homeassistant/components/auth/.translations/ro.json
Normal file
34
homeassistant/components/auth/.translations/ro.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "Nu sunt disponibile servicii de notificare."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Cod invalid, va rugam incercati din nou."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Selecta\u021bi unul dintre serviciile de notificare:",
|
||||
"title": "Configura\u021bi o parol\u0103 unic\u0103 livrat\u0103 de o component\u0103 de notificare"
|
||||
},
|
||||
"setup": {
|
||||
"description": "O parol\u0103 unic\u0103 a fost trimis\u0103 prin **notify.{notify_service}**. Introduce\u021bi parola mai jos:",
|
||||
"title": "Verifica\u021bi configurarea"
|
||||
}
|
||||
},
|
||||
"title": "Notifica\u021bi o parol\u0103 unic\u0103"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Cod invalid, va rugam incercati din nou. Dac\u0103 primi\u021bi aceast\u0103 eroare \u00een mod consecvent, asigura\u021bi-v\u0103 c\u0103 ceasul sistemului dvs. Home Assistant este corect."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Configura\u021bi autentificarea cu doi factori utiliz\u00e2nd TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
74
homeassistant/components/automation/geo_location.py
Normal file
74
homeassistant/components/automation/geo_location.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""
|
||||
Offer geo location automation rules.
|
||||
|
||||
For more details about this automation trigger, please refer to the
|
||||
documentation at
|
||||
https://home-assistant.io/docs/automation/trigger/#geo-location-trigger
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.geo_location import DOMAIN
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE, EVENT_STATE_CHANGED)
|
||||
from homeassistant.helpers import (
|
||||
condition, config_validation as cv)
|
||||
from homeassistant.helpers.config_validation import entity_domain
|
||||
|
||||
EVENT_ENTER = 'enter'
|
||||
EVENT_LEAVE = 'leave'
|
||||
DEFAULT_EVENT = EVENT_ENTER
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'geo_location',
|
||||
vol.Required(CONF_SOURCE): cv.string,
|
||||
vol.Required(CONF_ZONE): entity_domain('zone'),
|
||||
vol.Required(CONF_EVENT, default=DEFAULT_EVENT):
|
||||
vol.Any(EVENT_ENTER, EVENT_LEAVE),
|
||||
})
|
||||
|
||||
|
||||
def source_match(state, source):
|
||||
"""Check if the state matches the provided source."""
|
||||
return state and state.attributes.get('source') == source
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
source = config.get(CONF_SOURCE).lower()
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
trigger_event = config.get(CONF_EVENT)
|
||||
|
||||
@callback
|
||||
def state_change_listener(event):
|
||||
"""Handle specific state changes."""
|
||||
# Skip if the event is not a geo_location entity.
|
||||
if not event.data.get('entity_id').startswith(DOMAIN):
|
||||
return
|
||||
# Skip if the event's source does not match the trigger's source.
|
||||
from_state = event.data.get('old_state')
|
||||
to_state = event.data.get('new_state')
|
||||
if not source_match(from_state, source) \
|
||||
and not source_match(to_state, source):
|
||||
return
|
||||
|
||||
zone_state = hass.states.get(zone_entity_id)
|
||||
from_match = condition.zone(hass, zone_state, from_state)
|
||||
to_match = condition.zone(hass, zone_state, to_state)
|
||||
|
||||
# pylint: disable=too-many-boolean-expressions
|
||||
if trigger_event == EVENT_ENTER and not from_match and to_match or \
|
||||
trigger_event == EVENT_LEAVE and from_match and not to_match:
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'geo_location',
|
||||
'source': source,
|
||||
'entity_id': event.data.get('entity_id'),
|
||||
'from_state': from_state,
|
||||
'to_state': to_state,
|
||||
'zone': zone_state,
|
||||
'event': trigger_event,
|
||||
},
|
||||
}, context=event.context))
|
||||
|
||||
return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener)
|
@ -11,13 +11,12 @@ from aiohttp import hdrs
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ('webhook',)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_WEBHOOK_ID = 'webhook_id'
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'webhook',
|
||||
|
@ -38,6 +38,7 @@ AXIS_INCLUDE = EVENT_TYPES + PLATFORMS
|
||||
AXIS_DEFAULT_HOST = '192.168.0.90'
|
||||
AXIS_DEFAULT_USERNAME = 'root'
|
||||
AXIS_DEFAULT_PASSWORD = 'pass'
|
||||
DEFAULT_PORT = 80
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_INCLUDE):
|
||||
@ -47,7 +48,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(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(ATTR_LOCATION, default=''): cv.string,
|
||||
})
|
||||
|
||||
|
@ -4,16 +4,26 @@ Support for August binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.august/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.components.august import DATA_AUGUST
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['august']
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
|
||||
def _retrieve_door_state(data, lock):
|
||||
"""Get the latest state of the DoorSense sensor."""
|
||||
from august.lock import LockDoorStatus
|
||||
doorstate = data.get_door_state(lock.device_id)
|
||||
return doorstate == LockDoorStatus.OPEN
|
||||
|
||||
|
||||
def _retrieve_online_state(data, doorbell):
|
||||
"""Get the latest state of the sensor."""
|
||||
detail = data.get_doorbell_detail(doorbell.device_id)
|
||||
@ -46,7 +56,11 @@ def _activity_time_based_state(data, doorbell, activity_types):
|
||||
|
||||
|
||||
# Sensor types: Name, device_class, state_provider
|
||||
SENSOR_TYPES = {
|
||||
SENSOR_TYPES_DOOR = {
|
||||
'door_open': ['Open', 'door', _retrieve_door_state],
|
||||
}
|
||||
|
||||
SENSOR_TYPES_DOORBELL = {
|
||||
'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state],
|
||||
'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state],
|
||||
'doorbell_online': ['Online', 'connectivity', _retrieve_online_state],
|
||||
@ -58,14 +72,78 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
data = hass.data[DATA_AUGUST]
|
||||
devices = []
|
||||
|
||||
from august.lock import LockDoorStatus
|
||||
for door in data.locks:
|
||||
for sensor_type in SENSOR_TYPES_DOOR:
|
||||
state_provider = SENSOR_TYPES_DOOR[sensor_type][2]
|
||||
if state_provider(data, door) is LockDoorStatus.UNKNOWN:
|
||||
_LOGGER.debug(
|
||||
"Not adding sensor class %s for lock %s ",
|
||||
SENSOR_TYPES_DOOR[sensor_type][1], door.device_name
|
||||
)
|
||||
continue
|
||||
|
||||
_LOGGER.debug(
|
||||
"Adding sensor class %s for %s",
|
||||
SENSOR_TYPES_DOOR[sensor_type][1], door.device_name
|
||||
)
|
||||
devices.append(AugustDoorBinarySensor(data, sensor_type, door))
|
||||
|
||||
for doorbell in data.doorbells:
|
||||
for sensor_type in SENSOR_TYPES:
|
||||
devices.append(AugustBinarySensor(data, sensor_type, doorbell))
|
||||
for sensor_type in SENSOR_TYPES_DOORBELL:
|
||||
_LOGGER.debug("Adding doorbell sensor class %s for %s",
|
||||
SENSOR_TYPES_DOORBELL[sensor_type][1],
|
||||
doorbell.device_name)
|
||||
devices.append(
|
||||
AugustDoorbellBinarySensor(data, sensor_type,
|
||||
doorbell)
|
||||
)
|
||||
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class AugustBinarySensor(BinarySensorDevice):
|
||||
class AugustDoorBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an August Door binary sensor."""
|
||||
|
||||
def __init__(self, data, sensor_type, door):
|
||||
"""Initialize the sensor."""
|
||||
self._data = data
|
||||
self._sensor_type = sensor_type
|
||||
self._door = door
|
||||
self._state = None
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return the availability of this sensor."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return SENSOR_TYPES_DOOR[self._sensor_type][1]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return "{} {}".format(self._door.device_name,
|
||||
SENSOR_TYPES_DOOR[self._sensor_type][0])
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2]
|
||||
self._state = state_provider(self._data, self._door)
|
||||
|
||||
from august.lock import LockDoorStatus
|
||||
self._available = self._state != LockDoorStatus.UNKNOWN
|
||||
|
||||
|
||||
class AugustDoorbellBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an August binary sensor."""
|
||||
|
||||
def __init__(self, data, sensor_type, doorbell):
|
||||
@ -83,15 +161,15 @@ class AugustBinarySensor(BinarySensorDevice):
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return SENSOR_TYPES[self._sensor_type][1]
|
||||
return SENSOR_TYPES_DOORBELL[self._sensor_type][1]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return "{} {}".format(self._doorbell.device_name,
|
||||
SENSOR_TYPES[self._sensor_type][0])
|
||||
SENSOR_TYPES_DOORBELL[self._sensor_type][0])
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
state_provider = SENSOR_TYPES[self._sensor_type][2]
|
||||
state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2]
|
||||
self._state = state_provider(self._data, self._doorbell)
|
||||
|
@ -50,6 +50,12 @@ class BloomSkySensor(BinarySensorDevice):
|
||||
self._sensor_name = sensor_name
|
||||
self._name = '{} {}'.format(device['DeviceName'], sensor_name)
|
||||
self._state = None
|
||||
self._unique_id = '{}-{}'.format(self._device_id, self._sensor_name)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -8,6 +8,7 @@ import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
|
||||
from homeassistant.const import LENGTH_KILOMETERS
|
||||
|
||||
DEPENDENCIES = ['bmw_connected_drive']
|
||||
|
||||
@ -117,7 +118,8 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
result['lights_parking'] = vehicle_state.parking_lights.value
|
||||
elif self._attribute == 'condition_based_services':
|
||||
for report in vehicle_state.condition_based_services:
|
||||
result.update(self._format_cbs_report(report))
|
||||
result.update(
|
||||
self._format_cbs_report(report))
|
||||
elif self._attribute == 'check_control_messages':
|
||||
check_control_messages = vehicle_state.check_control_messages
|
||||
if not check_control_messages:
|
||||
@ -175,8 +177,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
self._state = (vehicle_state._attributes['connectionStatus'] ==
|
||||
'CONNECTED')
|
||||
|
||||
@staticmethod
|
||||
def _format_cbs_report(report):
|
||||
def _format_cbs_report(self, report):
|
||||
result = {}
|
||||
service_type = report.service_type.lower().replace('_', ' ')
|
||||
result['{} status'.format(service_type)] = report.state.value
|
||||
@ -184,8 +185,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
result['{} date'.format(service_type)] = \
|
||||
report.due_date.strftime('%Y-%m-%d')
|
||||
if report.due_distance is not None:
|
||||
result['{} distance'.format(service_type)] = \
|
||||
'{} km'.format(report.due_distance)
|
||||
distance = round(self.hass.config.units.length(
|
||||
report.due_distance, LENGTH_KILOMETERS))
|
||||
result['{} distance'.format(service_type)] = '{} {}'.format(
|
||||
distance, self.hass.config.units.length_unit)
|
||||
return result
|
||||
|
||||
def update_callback(self):
|
||||
|
@ -69,7 +69,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
|
||||
entities = []
|
||||
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
device = hass.data[DATA_KNX].xknx.devices[device_name]
|
||||
entities.append(KNXBinarySensor(hass, device))
|
||||
entities.append(KNXBinarySensor(device))
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@ -87,7 +87,7 @@ def async_add_entities_config(hass, config, async_add_entities):
|
||||
reset_after=config.get(CONF_RESET_AFTER))
|
||||
hass.data[DATA_KNX].xknx.devices.add(binary_sensor)
|
||||
|
||||
entity = KNXBinarySensor(hass, binary_sensor)
|
||||
entity = KNXBinarySensor(binary_sensor)
|
||||
automations = config.get(CONF_AUTOMATION)
|
||||
if automations is not None:
|
||||
for automation in automations:
|
||||
@ -103,11 +103,9 @@ def async_add_entities_config(hass, config, async_add_entities):
|
||||
class KNXBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a KNX binary sensor."""
|
||||
|
||||
def __init__(self, hass, device):
|
||||
def __init__(self, device):
|
||||
"""Initialize of KNX binary sensor."""
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
self.automations = []
|
||||
|
||||
@callback
|
||||
@ -118,6 +116,10 @@ class KNXBinarySensor(BinarySensorDevice):
|
||||
await self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Store register state change callback."""
|
||||
self.async_register_callbacks()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
|
@ -15,19 +15,21 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON,
|
||||
CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS)
|
||||
CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS, CONF_DEVICE)
|
||||
from homeassistant.components.mqtt import (
|
||||
ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC,
|
||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
|
||||
MqttAvailability, MqttDiscoveryUpdate)
|
||||
MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo)
|
||||
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.helpers.event as evt
|
||||
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'MQTT Binary sensor'
|
||||
CONF_OFF_DELAY = 'off_delay'
|
||||
CONF_UNIQUE_ID = 'unique_id'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
@ -41,9 +43,12 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
|
||||
vol.Optional(CONF_OFF_DELAY):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
# Integrations shouldn't never expose unique_id through configuration
|
||||
# this here is an exception because MQTT is a msg transport, not a protocol
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
@ -80,28 +85,32 @@ async def _async_setup_entity(hass, config, async_add_entities,
|
||||
config.get(CONF_DEVICE_CLASS),
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_FORCE_UPDATE),
|
||||
config.get(CONF_OFF_DELAY),
|
||||
config.get(CONF_PAYLOAD_ON),
|
||||
config.get(CONF_PAYLOAD_OFF),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
value_template,
|
||||
config.get(CONF_UNIQUE_ID),
|
||||
config.get(CONF_DEVICE),
|
||||
discovery_hash,
|
||||
)])
|
||||
|
||||
|
||||
class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
|
||||
BinarySensorDevice):
|
||||
MqttEntityDeviceInfo, BinarySensorDevice):
|
||||
"""Representation a binary sensor that is updated by MQTT."""
|
||||
|
||||
def __init__(self, name, state_topic, availability_topic, device_class,
|
||||
qos, force_update, payload_on, payload_off, payload_available,
|
||||
payload_not_available, value_template,
|
||||
unique_id: Optional[str], discovery_hash):
|
||||
qos, force_update, off_delay, payload_on, payload_off,
|
||||
payload_available, payload_not_available, value_template,
|
||||
unique_id: Optional[str], device_config: Optional[ConfigType],
|
||||
discovery_hash):
|
||||
"""Initialize the MQTT binary sensor."""
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash)
|
||||
MqttEntityDeviceInfo.__init__(self, device_config)
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._state_topic = state_topic
|
||||
@ -110,9 +119,11 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
|
||||
self._payload_off = payload_off
|
||||
self._qos = qos
|
||||
self._force_update = force_update
|
||||
self._off_delay = off_delay
|
||||
self._template = value_template
|
||||
self._unique_id = unique_id
|
||||
self._discovery_hash = discovery_hash
|
||||
self._delay_listener = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events."""
|
||||
@ -135,6 +146,20 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
|
||||
self._name, self._state_topic)
|
||||
return
|
||||
|
||||
if (self._state and self._off_delay is not None):
|
||||
@callback
|
||||
def off_delay_listener(now):
|
||||
"""Switch device off after a delay."""
|
||||
self._delay_listener = None
|
||||
self._state = False
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._delay_listener is not None:
|
||||
self._delay_listener()
|
||||
|
||||
self._delay_listener = evt.async_call_later(
|
||||
self.hass, self._off_delay, off_delay_listener)
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
await mqtt.async_subscribe(
|
||||
|
@ -7,45 +7,33 @@ https://home-assistant.io/components/binary_sensor.octoprint/
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.octoprint import (BINARY_SENSOR_TYPES,
|
||||
DOMAIN as COMPONENT_DOMAIN)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['octoprint']
|
||||
DOMAIN = "octoprint"
|
||||
DEFAULT_NAME = 'OctoPrint'
|
||||
|
||||
SENSOR_TYPES = {
|
||||
# API Endpoint, Group, Key, unit
|
||||
'Printing': ['printer', 'state', 'printing', None],
|
||||
'Printing Error': ['printer', 'state', 'error', None]
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the available OctoPrint binary sensors."""
|
||||
octoprint_api = hass.data[DOMAIN]["api"]
|
||||
name = config.get(CONF_NAME)
|
||||
monitored_conditions = config.get(
|
||||
CONF_MONITORED_CONDITIONS, SENSOR_TYPES.keys())
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
name = discovery_info['name']
|
||||
base_url = discovery_info['base_url']
|
||||
monitored_conditions = discovery_info['sensors']
|
||||
octoprint_api = hass.data[COMPONENT_DOMAIN][base_url]
|
||||
|
||||
devices = []
|
||||
for octo_type in monitored_conditions:
|
||||
new_sensor = OctoPrintBinarySensor(
|
||||
octoprint_api, octo_type, SENSOR_TYPES[octo_type][2],
|
||||
name, SENSOR_TYPES[octo_type][3], SENSOR_TYPES[octo_type][0],
|
||||
SENSOR_TYPES[octo_type][1], 'flags')
|
||||
octoprint_api, octo_type, BINARY_SENSOR_TYPES[octo_type][2],
|
||||
name, BINARY_SENSOR_TYPES[octo_type][3],
|
||||
BINARY_SENSOR_TYPES[octo_type][0],
|
||||
BINARY_SENSOR_TYPES[octo_type][1], 'flags')
|
||||
devices.append(new_sensor)
|
||||
add_entities(devices, True)
|
||||
|
||||
|
145
homeassistant/components/binary_sensor/opentherm_gw.py
Normal file
145
homeassistant/components/binary_sensor/opentherm_gw.py
Normal file
@ -0,0 +1,145 @@
|
||||
"""
|
||||
Support for OpenTherm Gateway binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
http://home-assistant.io/components/binary_sensor.opentherm_gw/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT)
|
||||
from homeassistant.components.opentherm_gw import (
|
||||
DATA_GW_VARS, DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
|
||||
DEVICE_CLASS_COLD = 'cold'
|
||||
DEVICE_CLASS_HEAT = 'heat'
|
||||
DEVICE_CLASS_PROBLEM = 'problem'
|
||||
|
||||
DEPENDENCIES = ['opentherm_gw']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the OpenTherm Gateway binary sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS]
|
||||
sensor_info = {
|
||||
# [device_class, friendly_name]
|
||||
gw_vars.DATA_MASTER_CH_ENABLED: [
|
||||
None, "Thermostat Central Heating Enabled"],
|
||||
gw_vars.DATA_MASTER_DHW_ENABLED: [
|
||||
None, "Thermostat Hot Water Enabled"],
|
||||
gw_vars.DATA_MASTER_COOLING_ENABLED: [
|
||||
None, "Thermostat Cooling Enabled"],
|
||||
gw_vars.DATA_MASTER_OTC_ENABLED: [
|
||||
None, "Thermostat Outside Temperature Correction Enabled"],
|
||||
gw_vars.DATA_MASTER_CH2_ENABLED: [
|
||||
None, "Thermostat Central Heating 2 Enabled"],
|
||||
gw_vars.DATA_SLAVE_FAULT_IND: [
|
||||
DEVICE_CLASS_PROBLEM, "Boiler Fault Indication"],
|
||||
gw_vars.DATA_SLAVE_CH_ACTIVE: [
|
||||
DEVICE_CLASS_HEAT, "Boiler Central Heating Status"],
|
||||
gw_vars.DATA_SLAVE_DHW_ACTIVE: [
|
||||
DEVICE_CLASS_HEAT, "Boiler Hot Water Status"],
|
||||
gw_vars.DATA_SLAVE_FLAME_ON: [
|
||||
DEVICE_CLASS_HEAT, "Boiler Flame Status"],
|
||||
gw_vars.DATA_SLAVE_COOLING_ACTIVE: [
|
||||
DEVICE_CLASS_COLD, "Boiler Cooling Status"],
|
||||
gw_vars.DATA_SLAVE_CH2_ACTIVE: [
|
||||
DEVICE_CLASS_HEAT, "Boiler Central Heating 2 Status"],
|
||||
gw_vars.DATA_SLAVE_DIAG_IND: [
|
||||
DEVICE_CLASS_PROBLEM, "Boiler Diagnostics Indication"],
|
||||
gw_vars.DATA_SLAVE_DHW_PRESENT: [None, "Boiler Hot Water Present"],
|
||||
gw_vars.DATA_SLAVE_CONTROL_TYPE: [None, "Boiler Control Type"],
|
||||
gw_vars.DATA_SLAVE_COOLING_SUPPORTED: [None, "Boiler Cooling Support"],
|
||||
gw_vars.DATA_SLAVE_DHW_CONFIG: [
|
||||
None, "Boiler Hot Water Configuration"],
|
||||
gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP: [
|
||||
None, "Boiler Pump Commands Support"],
|
||||
gw_vars.DATA_SLAVE_CH2_PRESENT: [
|
||||
None, "Boiler Central Heating 2 Present"],
|
||||
gw_vars.DATA_SLAVE_SERVICE_REQ: [
|
||||
DEVICE_CLASS_PROBLEM, "Boiler Service Required"],
|
||||
gw_vars.DATA_SLAVE_REMOTE_RESET: [None, "Boiler Remote Reset Support"],
|
||||
gw_vars.DATA_SLAVE_LOW_WATER_PRESS: [
|
||||
DEVICE_CLASS_PROBLEM, "Boiler Low Water Pressure"],
|
||||
gw_vars.DATA_SLAVE_GAS_FAULT: [
|
||||
DEVICE_CLASS_PROBLEM, "Boiler Gas Fault"],
|
||||
gw_vars.DATA_SLAVE_AIR_PRESS_FAULT: [
|
||||
DEVICE_CLASS_PROBLEM, "Boiler Air Pressure Fault"],
|
||||
gw_vars.DATA_SLAVE_WATER_OVERTEMP: [
|
||||
DEVICE_CLASS_PROBLEM, "Boiler Water Overtemperature"],
|
||||
gw_vars.DATA_REMOTE_TRANSFER_DHW: [
|
||||
None, "Remote Hot Water Setpoint Transfer Support"],
|
||||
gw_vars.DATA_REMOTE_TRANSFER_MAX_CH: [
|
||||
None, "Remote Maximum Central Heating Setpoint Write Support"],
|
||||
gw_vars.DATA_REMOTE_RW_DHW: [
|
||||
None, "Remote Hot Water Setpoint Write Support"],
|
||||
gw_vars.DATA_REMOTE_RW_MAX_CH: [
|
||||
None, "Remote Central Heating Setpoint Write Support"],
|
||||
gw_vars.DATA_ROVRD_MAN_PRIO: [
|
||||
None, "Remote Override Manual Change Priority"],
|
||||
gw_vars.DATA_ROVRD_AUTO_PRIO: [
|
||||
None, "Remote Override Program Change Priority"],
|
||||
gw_vars.OTGW_GPIO_A_STATE: [None, "Gateway GPIO A State"],
|
||||
gw_vars.OTGW_GPIO_B_STATE: [None, "Gateway GPIO B State"],
|
||||
gw_vars.OTGW_IGNORE_TRANSITIONS: [None, "Gateway Ignore Transitions"],
|
||||
gw_vars.OTGW_OVRD_HB: [None, "Gateway Override High Byte"],
|
||||
}
|
||||
sensors = []
|
||||
for var in discovery_info:
|
||||
device_class = sensor_info[var][0]
|
||||
friendly_name = sensor_info[var][1]
|
||||
entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, var, hass=hass)
|
||||
sensors.append(OpenThermBinarySensor(entity_id, var, device_class,
|
||||
friendly_name))
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class OpenThermBinarySensor(BinarySensorDevice):
|
||||
"""Represent an OpenTherm Gateway binary sensor."""
|
||||
|
||||
def __init__(self, entity_id, var, device_class, friendly_name):
|
||||
"""Initialize the binary sensor."""
|
||||
self.entity_id = entity_id
|
||||
self._var = var
|
||||
self._state = None
|
||||
self._device_class = device_class
|
||||
self._friendly_name = friendly_name
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to updates from the component."""
|
||||
_LOGGER.debug(
|
||||
"Added OpenTherm Gateway binary sensor %s", self._friendly_name)
|
||||
async_dispatcher_connect(self.hass, SIGNAL_OPENTHERM_GW_UPDATE,
|
||||
self.receive_report)
|
||||
|
||||
async def receive_report(self, status):
|
||||
"""Handle status updates from the component."""
|
||||
self._state = bool(status.get(self._var))
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the friendly name."""
|
||||
return self._friendly_name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False because entity pushes its state."""
|
||||
return False
|
@ -50,12 +50,12 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(openuv)
|
||||
|
||||
self._async_unsub_dispatcher_connect = None
|
||||
self._entry_id = entry_id
|
||||
self._icon = icon
|
||||
self._latitude = openuv.client.latitude
|
||||
self._longitude = openuv.client.longitude
|
||||
self._name = name
|
||||
self._dispatch_remove = None
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
|
||||
@ -80,16 +80,20 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
|
||||
return '{0}_{1}_{2}'.format(
|
||||
self._latitude, self._longitude, self._sensor_type)
|
||||
|
||||
@callback
|
||||
def _update_data(self):
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self._dispatch_remove = async_dispatcher_connect(
|
||||
self.hass, TOPIC_UPDATE, self._update_data)
|
||||
self.async_on_remove(self._dispatch_remove)
|
||||
@callback
|
||||
def update():
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
|
||||
self.hass, TOPIC_UPDATE, update)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect dispatcher listener when removed."""
|
||||
if self._async_unsub_dispatcher_connect:
|
||||
self._async_unsub_dispatcher_connect()
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the state."""
|
||||
|
@ -4,19 +4,18 @@ Tracks the latency of a host by sending ICMP echo requests (ping).
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.ping/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import CONF_NAME, CONF_HOST
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -49,14 +48,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Ping Binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
count = config.get(CONF_PING_COUNT)
|
||||
|
||||
async_add_entities([PingBinarySensor(name, PingData(host, count))], True)
|
||||
add_entities([PingBinarySensor(name, PingData(host, count))], True)
|
||||
|
||||
|
||||
class PingBinarySensor(BinarySensorDevice):
|
||||
@ -93,9 +91,9 @@ class PingBinarySensor(BinarySensorDevice):
|
||||
ATTR_ROUND_TRIP_TIME_MIN: self.ping.data['min'],
|
||||
}
|
||||
|
||||
async def async_update(self):
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
await self.ping.update()
|
||||
self.ping.update()
|
||||
|
||||
|
||||
class PingData:
|
||||
@ -116,13 +114,12 @@ class PingData:
|
||||
'ping', '-n', '-q', '-c', str(self._count), '-W1',
|
||||
self._ip_address]
|
||||
|
||||
async def ping(self):
|
||||
def ping(self):
|
||||
"""Send ICMP echo request and return details if success."""
|
||||
pinger = await asyncio.create_subprocess_shell(
|
||||
' '.join(self._ping_cmd), stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
pinger = subprocess.Popen(
|
||||
self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
try:
|
||||
out = await pinger.communicate()
|
||||
out = pinger.communicate()
|
||||
_LOGGER.debug("Output is %s", str(out))
|
||||
if sys.platform == 'win32':
|
||||
match = WIN32_PING_MATCHER.search(str(out).split('\n')[-1])
|
||||
@ -131,8 +128,7 @@ class PingData:
|
||||
'min': rtt_min,
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': '',
|
||||
}
|
||||
'mdev': ''}
|
||||
if 'max/' not in str(out):
|
||||
match = PING_MATCHER_BUSYBOX.search(str(out).split('\n')[-1])
|
||||
rtt_min, rtt_avg, rtt_max = match.groups()
|
||||
@ -140,20 +136,18 @@ class PingData:
|
||||
'min': rtt_min,
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': '',
|
||||
}
|
||||
'mdev': ''}
|
||||
match = PING_MATCHER.search(str(out).split('\n')[-1])
|
||||
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
|
||||
return {
|
||||
'min': rtt_min,
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': rtt_mdev,
|
||||
}
|
||||
'mdev': rtt_mdev}
|
||||
except (subprocess.CalledProcessError, AttributeError):
|
||||
return False
|
||||
|
||||
async def update(self):
|
||||
def update(self):
|
||||
"""Retrieve the latest details from the host."""
|
||||
self.data = await self.ping()
|
||||
self.data = self.ping()
|
||||
self.available = bool(self.data)
|
||||
|
105
homeassistant/components/binary_sensor/rflink.py
Normal file
105
homeassistant/components/binary_sensor/rflink.py
Normal file
@ -0,0 +1,105 @@
|
||||
"""
|
||||
Support for Rflink binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rflink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.components.rflink import (
|
||||
CONF_ALIASES, CONF_DEVICES, RflinkDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_FORCE_UPDATE, CONF_NAME, CONF_DEVICE_CLASS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.helpers.event as evt
|
||||
|
||||
CONF_OFF_DELAY = 'off_delay'
|
||||
DEFAULT_FORCE_UPDATE = False
|
||||
|
||||
DEPENDENCIES = ['rflink']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_DEVICES, default={}): {
|
||||
cv.string: vol.Schema({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE):
|
||||
cv.boolean,
|
||||
vol.Optional(CONF_OFF_DELAY): cv.positive_int,
|
||||
vol.Optional(CONF_ALIASES, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
},
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def devices_from_config(domain_config):
|
||||
"""Parse configuration and add Rflink sensor devices."""
|
||||
devices = []
|
||||
for device_id, config in domain_config[CONF_DEVICES].items():
|
||||
device = RflinkBinarySensor(device_id, **config)
|
||||
devices.append(device)
|
||||
|
||||
return devices
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Rflink platform."""
|
||||
async_add_entities(devices_from_config(config))
|
||||
|
||||
|
||||
class RflinkBinarySensor(RflinkDevice, BinarySensorDevice):
|
||||
"""Representation of an Rflink binary sensor."""
|
||||
|
||||
def __init__(self, device_id, device_class=None,
|
||||
force_update=None, off_delay=None,
|
||||
**kwargs):
|
||||
"""Handle sensor specific args and super init."""
|
||||
self._state = None
|
||||
self._device_class = device_class
|
||||
self._force_update = force_update
|
||||
self._off_delay = off_delay
|
||||
self._delay_listener = None
|
||||
super().__init__(device_id, **kwargs)
|
||||
|
||||
def _handle_event(self, event):
|
||||
"""Domain specific event handler."""
|
||||
command = event['command']
|
||||
if command == 'on':
|
||||
self._state = True
|
||||
elif command == 'off':
|
||||
self._state = False
|
||||
|
||||
if (self._state and self._off_delay is not None):
|
||||
def off_delay_listener(now):
|
||||
"""Switch device off after a delay."""
|
||||
self._delay_listener = None
|
||||
self._state = False
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._delay_listener is not None:
|
||||
self._delay_listener()
|
||||
self._delay_listener = evt.async_call_later(
|
||||
self.hass, self._off_delay, off_delay_listener)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def force_update(self):
|
||||
"""Force update."""
|
||||
return self._force_update
|
@ -73,6 +73,7 @@ class RingBinarySensor(BinarySensorDevice):
|
||||
SENSOR_TYPES.get(self._sensor_type)[0])
|
||||
self._device_class = SENSOR_TYPES.get(self._sensor_type)[2]
|
||||
self._state = None
|
||||
self._unique_id = '{}-{}'.format(self._data.id, self._sensor_type)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -89,6 +90,11 @@ class RingBinarySensor(BinarySensorDevice):
|
||||
"""Return the class of the binary sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
|
@ -22,7 +22,7 @@ from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
REQUIREMENTS = ['numpy==1.15.1']
|
||||
REQUIREMENTS = ['numpy==1.15.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -14,15 +14,16 @@ from homeassistant.const import CONF_NAME, WEEKDAYS
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['holidays==0.9.7']
|
||||
REQUIREMENTS = ['holidays==0.9.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# List of all countries currently supported by holidays
|
||||
# There seems to be no way to get the list out at runtime
|
||||
ALL_COUNTRIES = [
|
||||
'Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT', 'Belarus', 'BY'
|
||||
'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech', 'CZ',
|
||||
'Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT',
|
||||
'Brazil', 'BR', 'Belarus', 'BY', 'Belgium', 'BE',
|
||||
'Canada', 'CA', 'Colombia', 'CO', 'Croatia', 'HR', 'Czech', 'CZ',
|
||||
'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR',
|
||||
'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU',
|
||||
'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP',
|
||||
@ -30,7 +31,7 @@ ALL_COUNTRIES = [
|
||||
'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
|
||||
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK',
|
||||
'South Africa', 'ZA', 'Spain', 'ES', 'Sweden', 'SE', 'Switzerland', 'CH',
|
||||
'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales',
|
||||
'Ukraine', 'UA', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales',
|
||||
]
|
||||
|
||||
ALLOWED_DAYS = WEEKDAYS + ['holiday']
|
||||
|
@ -4,6 +4,8 @@ import logging
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY,
|
||||
XiaomiDevice)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -36,21 +38,24 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
elif model in ['natgas', 'sensor_natgas']:
|
||||
devices.append(XiaomiNatgasSensor(device, gateway))
|
||||
elif model in ['switch', 'sensor_switch',
|
||||
'sensor_switch.aq2', 'sensor_switch.aq3']:
|
||||
'sensor_switch.aq2', 'sensor_switch.aq3',
|
||||
'remote.b1acn01']:
|
||||
if 'proto' not in device or int(device['proto'][0:1]) == 1:
|
||||
data_key = 'status'
|
||||
else:
|
||||
data_key = 'button_0'
|
||||
devices.append(XiaomiButton(device, 'Switch', data_key,
|
||||
hass, gateway))
|
||||
elif model in ['86sw1', 'sensor_86sw1', 'sensor_86sw1.aq1']:
|
||||
elif model in ['86sw1', 'sensor_86sw1', 'sensor_86sw1.aq1',
|
||||
'remote.b186acn01']:
|
||||
if 'proto' not in device or int(device['proto'][0:1]) == 1:
|
||||
data_key = 'channel_0'
|
||||
else:
|
||||
data_key = 'button_0'
|
||||
devices.append(XiaomiButton(device, 'Wall Switch', data_key,
|
||||
hass, gateway))
|
||||
elif model in ['86sw2', 'sensor_86sw2', 'sensor_86sw2.aq1']:
|
||||
elif model in ['86sw2', 'sensor_86sw2', 'sensor_86sw2.aq1',
|
||||
'remote.b286acn01']:
|
||||
if 'proto' not in device or int(device['proto'][0:1]) == 1:
|
||||
data_key_left = 'channel_0'
|
||||
data_key_right = 'channel_1'
|
||||
@ -65,6 +70,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
'dual_channel', hass, gateway))
|
||||
elif model in ['cube', 'sensor_cube', 'sensor_cube.aqgl01']:
|
||||
devices.append(XiaomiCube(device, hass, gateway))
|
||||
elif model in ['vibration', 'vibration.aq1']:
|
||||
devices.append(XiaomiVibration(device, 'Vibration',
|
||||
'status', gateway))
|
||||
else:
|
||||
_LOGGER.warning('Unmapped Device Model %s', model)
|
||||
|
||||
add_entities(devices)
|
||||
|
||||
|
||||
@ -144,6 +155,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||
"""Initialize the XiaomiMotionSensor."""
|
||||
self._hass = hass
|
||||
self._no_motion_since = 0
|
||||
self._unsub_set_no_motion = None
|
||||
if 'proto' not in device or int(device['proto'][0:1]) == 1:
|
||||
data_key = 'status'
|
||||
else:
|
||||
@ -158,6 +170,13 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
@callback
|
||||
def _async_set_no_motion(self, now):
|
||||
"""Set state to False."""
|
||||
self._unsub_set_no_motion = None
|
||||
self._state = False
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def parse_data(self, data, raw_data):
|
||||
"""Parse data sent by gateway."""
|
||||
if raw_data['cmd'] == 'heartbeat':
|
||||
@ -179,11 +198,20 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||
return False
|
||||
|
||||
if value == MOTION:
|
||||
self._should_poll = True
|
||||
if self.entity_id is not None:
|
||||
self._hass.bus.fire('motion', {
|
||||
'entity_id': self.entity_id
|
||||
})
|
||||
if self._data_key == 'motion_status':
|
||||
if self._unsub_set_no_motion:
|
||||
self._unsub_set_no_motion()
|
||||
self._unsub_set_no_motion = async_call_later(
|
||||
self._hass,
|
||||
180,
|
||||
self._async_set_no_motion
|
||||
)
|
||||
else:
|
||||
self._should_poll = True
|
||||
if self.entity_id is not None:
|
||||
self._hass.bus.fire('motion', {
|
||||
'entity_id': self.entity_id
|
||||
})
|
||||
|
||||
self._no_motion_since = 0
|
||||
if self._state:
|
||||
@ -311,6 +339,38 @@ class XiaomiSmokeSensor(XiaomiBinarySensor):
|
||||
return False
|
||||
|
||||
|
||||
class XiaomiVibration(XiaomiBinarySensor):
|
||||
"""Representation of a Xiaomi Vibration Sensor."""
|
||||
|
||||
def __init__(self, device, name, data_key, xiaomi_hub):
|
||||
"""Initialize the XiaomiVibration."""
|
||||
self._last_action = None
|
||||
super().__init__(device, name, xiaomi_hub, data_key, None)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {ATTR_LAST_ACTION: self._last_action}
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data, raw_data):
|
||||
"""Parse data sent by gateway."""
|
||||
value = data.get(self._data_key)
|
||||
if value not in ('vibrate', 'tilt', 'free_fall'):
|
||||
_LOGGER.warning("Unsupported movement_type detected: %s",
|
||||
value)
|
||||
return False
|
||||
|
||||
self.hass.bus.fire('xiaomi_aqara.movement', {
|
||||
'entity_id': self.entity_id,
|
||||
'movement_type': value
|
||||
})
|
||||
self._last_action = value
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class XiaomiButton(XiaomiBinarySensor):
|
||||
"""Representation of a Xiaomi Button."""
|
||||
|
||||
|
@ -253,5 +253,9 @@ class Remote(zha.Entity, BinarySensorDevice):
|
||||
"""Retrieve latest state."""
|
||||
from zigpy.zcl.clusters.general import OnOff
|
||||
result = await zha.safe_read(
|
||||
self._endpoint.out_clusters[OnOff.cluster_id], ['on_off'])
|
||||
self._endpoint.out_clusters[OnOff.cluster_id],
|
||||
['on_off'],
|
||||
allow_cache=False,
|
||||
only_cache=(not self._initialized)
|
||||
)
|
||||
self._state = result.get('on_off', self._state)
|
||||
|
@ -7,10 +7,11 @@ https://home-assistant.io/components/binary_sensor.zwave/
|
||||
import logging
|
||||
import datetime
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import
|
||||
async_setup_platform, workaround)
|
||||
from homeassistant.components.zwave import workaround
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN,
|
||||
BinarySensorDevice)
|
||||
@ -19,6 +20,23 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Old method of setting up Z-Wave binary sensors."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up Z-Wave binary sensors from Config Entry."""
|
||||
@callback
|
||||
def async_add_binary_sensor(binary_sensor):
|
||||
"""Add Z-Wave binary sensor."""
|
||||
async_add_entities([binary_sensor])
|
||||
|
||||
async_dispatcher_connect(hass, 'zwave_new_binary_sensor',
|
||||
async_add_binary_sensor)
|
||||
|
||||
|
||||
def get_device(values, **kwargs):
|
||||
"""Create Z-Wave entity device."""
|
||||
device_mapping = workaround.get_device_mapping(values.primary)
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
|
||||
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
|
||||
|
||||
REQUIREMENTS = ['blinkpy==0.10.0']
|
||||
REQUIREMENTS = ['blinkpy==0.10.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -9,7 +9,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD)
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -85,6 +85,7 @@ def setup_account(account_config: dict, hass, name: str) \
|
||||
password = account_config[CONF_PASSWORD]
|
||||
region = account_config[CONF_REGION]
|
||||
read_only = account_config[CONF_READ_ONLY]
|
||||
|
||||
_LOGGER.debug('Adding new account %s', name)
|
||||
cd_account = BMWConnectedDriveAccount(username, password, region, name,
|
||||
read_only)
|
||||
|
@ -68,6 +68,7 @@ class GoogleCalendarData:
|
||||
self.event = None
|
||||
|
||||
def _prepare_query(self):
|
||||
# pylint: disable=import-error
|
||||
from httplib2 import ServerNotFoundError
|
||||
|
||||
try:
|
||||
|
@ -518,6 +518,8 @@ class TodoistProjectData:
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
if self._id is None:
|
||||
self._api.reset_state()
|
||||
self._api.sync()
|
||||
project_task_data = [
|
||||
task for task in self._api.state[TASKS]
|
||||
if not self._project_id_whitelist or
|
||||
@ -527,6 +529,7 @@ class TodoistProjectData:
|
||||
|
||||
# If we have no data, we can just return right away.
|
||||
if not project_task_data:
|
||||
_LOGGER.debug("No data for %s", self._name)
|
||||
self.event = None
|
||||
return True
|
||||
|
||||
@ -541,6 +544,8 @@ class TodoistProjectData:
|
||||
|
||||
if not project_tasks:
|
||||
# We had no valid tasks
|
||||
_LOGGER.debug("No valid tasks for %s", self._name)
|
||||
self.event = None
|
||||
return True
|
||||
|
||||
# Make sure the task collection is reset to prevent an
|
||||
|
@ -53,6 +53,11 @@ class BloomSkyCamera(Camera):
|
||||
|
||||
return self._last_image
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this BloomSky device."""
|
||||
|
@ -32,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up a FFmpeg camera."""
|
||||
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
|
||||
if not await hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
|
||||
return
|
||||
async_add_entities([FFmpegCamera(hass, config)])
|
||||
|
||||
|
@ -63,3 +63,8 @@ class NeatoCleaningMap(Camera):
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._robot_name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID."""
|
||||
return self._robot_serial
|
||||
|
@ -97,6 +97,11 @@ class RingCam(Camera):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._camera.id
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
|
@ -35,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_MODEL): vol.Any(MODEL_YI,
|
||||
MODEL_XIAOFANG),
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
|
@ -32,7 +32,7 @@ CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
|
@ -7,9 +7,9 @@
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Deseja configurar o Google Cast?",
|
||||
"title": ""
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": ""
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
15
homeassistant/components/cast/.translations/ro.json
Normal file
15
homeassistant/components/cast/.translations/ro.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Nu s-au g\u0103sit dispozitive Google Cast \u00een re\u021bea.",
|
||||
"single_instance_allowed": "Este necesar\u0103 o singur\u0103 configura\u021bie a serviciului Google Cast."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Dori\u021bi s\u0103 configura\u021bi Google Cast?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
@ -48,11 +48,6 @@ STATE_MANUAL = 'manual'
|
||||
STATE_DRY = 'dry'
|
||||
STATE_FAN_ONLY = 'fan_only'
|
||||
STATE_ECO = 'eco'
|
||||
STATE_ELECTRIC = 'electric'
|
||||
STATE_PERFORMANCE = 'performance'
|
||||
STATE_HIGH_DEMAND = 'high_demand'
|
||||
STATE_HEAT_PUMP = 'heat_pump'
|
||||
STATE_GAS = 'gas'
|
||||
|
||||
SUPPORT_TARGET_TEMPERATURE = 1
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH = 2
|
||||
|
@ -40,6 +40,15 @@ HA_STATE_TO_DAIKIN = {
|
||||
STATE_OFF: 'off',
|
||||
}
|
||||
|
||||
DAIKIN_TO_HA_STATE = {
|
||||
'fan': STATE_FAN_ONLY,
|
||||
'dry': STATE_DRY,
|
||||
'cool': STATE_COOL,
|
||||
'hot': STATE_HEAT,
|
||||
'auto': STATE_AUTO,
|
||||
'off': STATE_OFF,
|
||||
}
|
||||
|
||||
HA_ATTR_TO_DAIKIN = {
|
||||
ATTR_OPERATION_MODE: 'mode',
|
||||
ATTR_FAN_MODE: 'f_rate',
|
||||
@ -75,9 +84,7 @@ class DaikinClimate(ClimateDevice):
|
||||
self._api = api
|
||||
self._force_refresh = False
|
||||
self._list = {
|
||||
ATTR_OPERATION_MODE: list(
|
||||
map(str.title, set(HA_STATE_TO_DAIKIN.values()))
|
||||
),
|
||||
ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN),
|
||||
ATTR_FAN_MODE: list(
|
||||
map(
|
||||
str.title,
|
||||
@ -136,11 +143,11 @@ class DaikinClimate(ClimateDevice):
|
||||
elif key == ATTR_OPERATION_MODE:
|
||||
# Daikin can return also internal states auto-1 or auto-7
|
||||
# and we need to translate them as AUTO
|
||||
value = re.sub(
|
||||
'[^a-z]',
|
||||
'',
|
||||
self._api.device.represent(daikin_attr)[1]
|
||||
).title()
|
||||
daikin_mode = re.sub(
|
||||
'[^a-z]', '',
|
||||
self._api.device.represent(daikin_attr)[1])
|
||||
ha_mode = DAIKIN_TO_HA_STATE.get(daikin_mode)
|
||||
value = ha_mode
|
||||
|
||||
if value is None:
|
||||
_LOGGER.error("Invalid value requested for key %s", key)
|
||||
@ -167,8 +174,8 @@ class DaikinClimate(ClimateDevice):
|
||||
|
||||
daikin_attr = HA_ATTR_TO_DAIKIN.get(attr)
|
||||
if daikin_attr is not None:
|
||||
if value.title() in self._list[attr]:
|
||||
values[daikin_attr] = value.lower()
|
||||
if value in self._list[attr]:
|
||||
values[daikin_attr] = HA_STATE_TO_DAIKIN[value]
|
||||
else:
|
||||
_LOGGER.error("Invalid value %s for %s", attr, value)
|
||||
|
||||
|
176
homeassistant/components/climate/dyson.py
Normal file
176
homeassistant/components/climate/dyson.py
Normal file
@ -0,0 +1,176 @@
|
||||
"""
|
||||
Support for Dyson Pure Hot+Cool link fan.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.dyson/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.dyson import DYSON_DEVICES
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_DIFFUSE = "Diffuse Mode"
|
||||
STATE_FOCUS = "Focus Mode"
|
||||
FAN_LIST = [STATE_FOCUS, STATE_DIFFUSE]
|
||||
OPERATION_LIST = [STATE_HEAT, STATE_COOL]
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
|
||||
| SUPPORT_OPERATION_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Dyson fan components."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
from libpurecoollink.dyson_pure_hotcool_link import DysonPureHotCoolLink
|
||||
# Get Dyson Devices from parent component.
|
||||
add_devices(
|
||||
[DysonPureHotCoolLinkDevice(device)
|
||||
for device in hass.data[DYSON_DEVICES]
|
||||
if isinstance(device, DysonPureHotCoolLink)]
|
||||
)
|
||||
|
||||
|
||||
class DysonPureHotCoolLinkDevice(ClimateDevice):
|
||||
"""Representation of a Dyson climate fan."""
|
||||
|
||||
def __init__(self, device):
|
||||
"""Initialize the fan."""
|
||||
self._device = device
|
||||
self._current_temp = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity is added to hass."""
|
||||
self.hass.async_add_job(self._device.add_message_listener,
|
||||
self.on_message)
|
||||
|
||||
def on_message(self, message):
|
||||
"""Call when new messages received from the climate."""
|
||||
from libpurecoollink.dyson_pure_state import DysonPureHotCoolState
|
||||
|
||||
if isinstance(message, DysonPureHotCoolState):
|
||||
_LOGGER.debug("Message received for climate device %s : %s",
|
||||
self.name, message)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the display name of this climate."""
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
if self._device.environmental_state:
|
||||
temperature_kelvin = self._device.environmental_state.temperature
|
||||
if temperature_kelvin != 0:
|
||||
self._current_temp = float("{0:.1f}".format(
|
||||
temperature_kelvin - 273))
|
||||
return self._current_temp
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the target temperature."""
|
||||
heat_target = int(self._device.state.heat_target) / 10
|
||||
return int(heat_target - 273)
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
if self._device.environmental_state:
|
||||
if self._device.environmental_state.humidity == 0:
|
||||
return None
|
||||
return self._device.environmental_state.humidity
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
from libpurecoollink.const import HeatMode, HeatState
|
||||
if self._device.state.heat_mode == HeatMode.HEAT_ON.value:
|
||||
if self._device.state.heat_state == HeatState.HEAT_STATE_ON.value:
|
||||
return STATE_HEAT
|
||||
return STATE_IDLE
|
||||
return STATE_COOL
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return OPERATION_LIST
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
from libpurecoollink.const import FocusMode
|
||||
if self._device.state.focus_mode == FocusMode.FOCUS_ON.value:
|
||||
return STATE_FOCUS
|
||||
return STATE_DIFFUSE
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return FAN_LIST
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
if target_temp is None:
|
||||
return
|
||||
target_temp = int(target_temp)
|
||||
_LOGGER.debug("Set %s temperature %s", self.name, target_temp)
|
||||
# Limit the target temperature into acceptable range.
|
||||
target_temp = min(self.max_temp, target_temp)
|
||||
target_temp = max(self.min_temp, target_temp)
|
||||
from libpurecoollink.const import HeatTarget, HeatMode
|
||||
self._device.set_configuration(
|
||||
heat_target=HeatTarget.celsius(target_temp),
|
||||
heat_mode=HeatMode.HEAT_ON)
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new fan mode."""
|
||||
_LOGGER.debug("Set %s focus mode %s", self.name, fan_mode)
|
||||
from libpurecoollink.const import FocusMode
|
||||
if fan_mode == STATE_FOCUS:
|
||||
self._device.set_configuration(focus_mode=FocusMode.FOCUS_ON)
|
||||
elif fan_mode == STATE_DIFFUSE:
|
||||
self._device.set_configuration(focus_mode=FocusMode.FOCUS_OFF)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
_LOGGER.debug("Set %s heat mode %s", self.name, operation_mode)
|
||||
from libpurecoollink.const import HeatMode
|
||||
if operation_mode == STATE_HEAT:
|
||||
self._device.set_configuration(heat_mode=HeatMode.HEAT_ON)
|
||||
elif operation_mode == STATE_COOL:
|
||||
self._device.set_configuration(heat_mode=HeatMode.HEAT_OFF)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return 1
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return 37
|
193
homeassistant/components/climate/elkm1.py
Normal file
193
homeassistant/components/climate/elkm1.py
Normal file
@ -0,0 +1,193 @@
|
||||
"""
|
||||
Support for control of Elk-M1 connected thermostats.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.elkm1/
|
||||
"""
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PRECISION_WHOLE, STATE_AUTO,
|
||||
STATE_COOL, STATE_FAN_ONLY, STATE_HEAT, STATE_IDLE, SUPPORT_AUX_HEAT,
|
||||
SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE_HIGH,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice)
|
||||
from homeassistant.components.elkm1 import (
|
||||
DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities)
|
||||
from homeassistant.const import STATE_ON
|
||||
|
||||
DEPENDENCIES = [ELK_DOMAIN]
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Create the Elk-M1 thermostat platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
elk = hass.data[ELK_DOMAIN]['elk']
|
||||
async_add_entities(create_elk_entities(
|
||||
hass, elk.thermostats, 'thermostat', ElkThermostat, []), True)
|
||||
|
||||
|
||||
class ElkThermostat(ElkEntity, ClimateDevice):
|
||||
"""Representation of an Elk-M1 Thermostat."""
|
||||
|
||||
def __init__(self, element, elk, elk_data):
|
||||
"""Initialize climate entity."""
|
||||
super().__init__(element, elk, elk_data)
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return (SUPPORT_OPERATION_MODE | SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT
|
||||
| SUPPORT_TARGET_TEMPERATURE_HIGH
|
||||
| SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the temperature unit."""
|
||||
return self._temperature_unit
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._element.current_temp
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we are trying to reach."""
|
||||
from elkm1_lib.const import ThermostatMode
|
||||
if (self._element.mode == ThermostatMode.HEAT.value) or (
|
||||
self._element.mode == ThermostatMode.EMERGENCY_HEAT.value):
|
||||
return self._element.heat_setpoint
|
||||
if self._element.mode == ThermostatMode.COOL.value:
|
||||
return self._element.cool_setpoint
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the high target temperature."""
|
||||
return self._element.cool_setpoint
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the low target temperature."""
|
||||
return self._element.heat_setpoint
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return 1
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
return self._element.humidity
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return [STATE_IDLE, STATE_HEAT, STATE_COOL, STATE_AUTO, STATE_FAN_ONLY]
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return the precision of the system."""
|
||||
return PRECISION_WHOLE
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return if aux heater is on."""
|
||||
from elkm1_lib.const import ThermostatMode
|
||||
return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature supported."""
|
||||
return 1
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature supported."""
|
||||
return 99
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
from elkm1_lib.const import ThermostatFan
|
||||
if self._element.fan == ThermostatFan.AUTO.value:
|
||||
return STATE_AUTO
|
||||
if self._element.fan == ThermostatFan.ON.value:
|
||||
return STATE_ON
|
||||
return None
|
||||
|
||||
def _elk_set(self, mode, fan):
|
||||
from elkm1_lib.const import ThermostatSetting
|
||||
if mode is not None:
|
||||
self._element.set(ThermostatSetting.MODE.value, mode)
|
||||
if fan is not None:
|
||||
self._element.set(ThermostatSetting.FAN.value, fan)
|
||||
|
||||
async def async_set_operation_mode(self, operation_mode):
|
||||
"""Set thermostat operation mode."""
|
||||
from elkm1_lib.const import ThermostatFan, ThermostatMode
|
||||
settings = {
|
||||
STATE_IDLE: (ThermostatMode.OFF.value, ThermostatFan.AUTO.value),
|
||||
STATE_HEAT: (ThermostatMode.HEAT.value, None),
|
||||
STATE_COOL: (ThermostatMode.COOL.value, None),
|
||||
STATE_AUTO: (ThermostatMode.AUTO.value, None),
|
||||
STATE_FAN_ONLY: (ThermostatMode.OFF.value, ThermostatFan.ON.value)
|
||||
}
|
||||
self._elk_set(settings[operation_mode][0], settings[operation_mode][1])
|
||||
|
||||
async def async_turn_aux_heat_on(self):
|
||||
"""Turn auxiliary heater on."""
|
||||
from elkm1_lib.const import ThermostatMode
|
||||
self._elk_set(ThermostatMode.EMERGENCY_HEAT.value, None)
|
||||
|
||||
async def async_turn_aux_heat_off(self):
|
||||
"""Turn auxiliary heater off."""
|
||||
from elkm1_lib.const import ThermostatMode
|
||||
self._elk_set(ThermostatMode.HEAT.value, None)
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return [STATE_AUTO, STATE_ON]
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
from elkm1_lib.const import ThermostatFan
|
||||
if fan_mode == STATE_AUTO:
|
||||
self._elk_set(None, ThermostatFan.AUTO.value)
|
||||
elif fan_mode == STATE_ON:
|
||||
self._elk_set(None, ThermostatFan.ON.value)
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
from elkm1_lib.const import ThermostatSetting
|
||||
low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if low_temp is not None:
|
||||
self._element.set(
|
||||
ThermostatSetting.HEAT_SETPOINT.value, round(low_temp))
|
||||
if high_temp is not None:
|
||||
self._element.set(
|
||||
ThermostatSetting.COOL_SETPOINT.value, round(high_temp))
|
||||
|
||||
def _element_changed(self, element, changeset):
|
||||
from elkm1_lib.const import ThermostatFan, ThermostatMode
|
||||
mode_to_state = {
|
||||
ThermostatMode.OFF.value: STATE_IDLE,
|
||||
ThermostatMode.COOL.value: STATE_COOL,
|
||||
ThermostatMode.HEAT.value: STATE_HEAT,
|
||||
ThermostatMode.EMERGENCY_HEAT.value: STATE_HEAT,
|
||||
ThermostatMode.AUTO.value: STATE_AUTO,
|
||||
}
|
||||
self._state = mode_to_state.get(self._element.mode)
|
||||
if self._state == STATE_IDLE and \
|
||||
self._element.fan == ThermostatFan.ON.value:
|
||||
self._state = STATE_FAN_ONLY
|
@ -10,12 +10,13 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE)
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE,
|
||||
SUPPORT_ON_OFF)
|
||||
from homeassistant.const import (
|
||||
CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.41']
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.45']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -39,7 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_AWAY_MODE)
|
||||
SUPPORT_AWAY_MODE | SUPPORT_ON_OFF)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
@ -53,14 +54,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
add_entities(devices)
|
||||
|
||||
|
||||
# pylint: disable=import-error
|
||||
class EQ3BTSmartThermostat(ClimateDevice):
|
||||
"""Representation of an eQ-3 Bluetooth Smart thermostat."""
|
||||
|
||||
def __init__(self, _mac, _name):
|
||||
"""Initialize the thermostat."""
|
||||
# We want to avoid name clash with this module.
|
||||
import eq3bt as eq3
|
||||
import eq3bt as eq3 # pylint: disable=import-error
|
||||
|
||||
self.modes = {
|
||||
eq3.Mode.Open: STATE_ON,
|
||||
@ -151,6 +151,14 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
"""Return if we are away."""
|
||||
return self.current_operation == STATE_AWAY
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn device on."""
|
||||
self.set_operation_mode(STATE_AUTO)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn device off."""
|
||||
self.set_operation_mode(STATE_OFF)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
@ -176,7 +184,7 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
from bluepy.btle import BTLEException
|
||||
from bluepy.btle import BTLEException # pylint: disable=import-error
|
||||
try:
|
||||
self._thermostat.update()
|
||||
except BTLEException as ex:
|
||||
|
@ -75,7 +75,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
|
||||
entities = []
|
||||
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
device = hass.data[DATA_KNX].xknx.devices[device_name]
|
||||
entities.append(KNXClimate(hass, device))
|
||||
entities.append(KNXClimate(device))
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@ -110,17 +110,15 @@ def async_add_entities_config(hass, config, async_add_entities):
|
||||
group_address_operation_mode_comfort=config.get(
|
||||
CONF_OPERATION_MODE_COMFORT_ADDRESS))
|
||||
hass.data[DATA_KNX].xknx.devices.add(climate)
|
||||
async_add_entities([KNXClimate(hass, climate)])
|
||||
async_add_entities([KNXClimate(climate)])
|
||||
|
||||
|
||||
class KNXClimate(ClimateDevice):
|
||||
"""Representation of a KNX climate device."""
|
||||
|
||||
def __init__(self, hass, device):
|
||||
def __init__(self, device):
|
||||
"""Initialize of a KNX climate device."""
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
@ -137,6 +135,10 @@ class KNXClimate(ClimateDevice):
|
||||
await self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Store register state change callback."""
|
||||
self.async_register_callbacks()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
|
153
homeassistant/components/climate/mill.py
Normal file
153
homeassistant/components/climate/mill.py
Normal file
@ -0,0 +1,153 @@
|
||||
"""
|
||||
Support for mill wifi-enabled home heaters.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.mill/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_FAN_MODE, SUPPORT_ON_OFF)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_PASSWORD, CONF_USERNAME,
|
||||
STATE_ON, STATE_OFF, TEMP_CELSIUS)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
REQUIREMENTS = ['millheater==0.1.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MAX_TEMP = 35
|
||||
MIN_TEMP = 5
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_FAN_MODE | SUPPORT_ON_OFF)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Mill heater."""
|
||||
from mill import Mill
|
||||
mill_data_connection = Mill(config[CONF_USERNAME],
|
||||
config[CONF_PASSWORD],
|
||||
websession=async_get_clientsession(hass))
|
||||
if not await mill_data_connection.connect():
|
||||
_LOGGER.error("Failed to connect to Mill")
|
||||
return
|
||||
|
||||
await mill_data_connection.update_heaters()
|
||||
|
||||
dev = []
|
||||
for heater in mill_data_connection.heaters.values():
|
||||
dev.append(MillHeater(heater, mill_data_connection))
|
||||
async_add_entities(dev)
|
||||
|
||||
|
||||
class MillHeater(ClimateDevice):
|
||||
"""Representation of a Mill Thermostat device."""
|
||||
|
||||
def __init__(self, heater, mill_data_connection):
|
||||
"""Initialize the thermostat."""
|
||||
self._heater = heater
|
||||
self._conn = mill_data_connection
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._heater.device_status == 0 # weird api choice
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._heater.device_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._heater.name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement which this thermostat uses."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._heater.set_temp
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return 1
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._heater.current_temp
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return STATE_ON if self._heater.fan_status == 1 else STATE_OFF
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return [STATE_ON, STATE_OFF]
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if heater is on."""
|
||||
return self._heater.power_status == 1
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return MIN_TEMP
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return MAX_TEMP
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
await self._conn.set_heater_temp(self._heater.device_id,
|
||||
int(temperature))
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
fan_status = 1 if fan_mode == STATE_ON else 0
|
||||
await self._conn.heater_control(self._heater.device_id,
|
||||
fan_status=fan_status)
|
||||
|
||||
async def async_turn_on(self):
|
||||
"""Turn Mill unit on."""
|
||||
await self._conn.heater_control(self._heater.device_id,
|
||||
power_status=1)
|
||||
|
||||
async def async_turn_off(self):
|
||||
"""Turn Mill unit off."""
|
||||
await self._conn.heater_control(self._heater.device_id,
|
||||
power_status=0)
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._heater = await self._conn.update_device(self._heater.device_id)
|
@ -75,6 +75,7 @@ CONF_SEND_IF_OFF = 'send_if_off'
|
||||
|
||||
CONF_MIN_TEMP = 'min_temp'
|
||||
CONF_MAX_TEMP = 'max_temp'
|
||||
CONF_TEMP_STEP = 'temp_step'
|
||||
|
||||
SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema)
|
||||
PLATFORM_SCHEMA = SCHEMA_BASE.extend({
|
||||
@ -124,7 +125,8 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
|
||||
|
||||
vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float)
|
||||
vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float)
|
||||
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
@ -213,6 +215,7 @@ async def _async_setup_entity(hass, config, async_add_entities,
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
config.get(CONF_MIN_TEMP),
|
||||
config.get(CONF_MAX_TEMP),
|
||||
config.get(CONF_TEMP_STEP),
|
||||
discovery_hash,
|
||||
)])
|
||||
|
||||
@ -226,7 +229,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
||||
current_swing_mode, current_operation, aux, send_if_off,
|
||||
payload_on, payload_off, availability_topic,
|
||||
payload_available, payload_not_available,
|
||||
min_temp, max_temp, discovery_hash):
|
||||
min_temp, max_temp, temp_step, discovery_hash):
|
||||
"""Initialize the climate device."""
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
@ -237,19 +240,26 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
||||
self._value_templates = value_templates
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
self._target_temperature = target_temperature
|
||||
# set to None in non-optimistic mode
|
||||
self._target_temperature = self._current_fan_mode = \
|
||||
self._current_operation = self._current_swing_mode = None
|
||||
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None:
|
||||
self._target_temperature = target_temperature
|
||||
self._unit_of_measurement = hass.config.units.temperature_unit
|
||||
self._away = away
|
||||
self._hold = hold
|
||||
self._current_temperature = None
|
||||
self._current_fan_mode = current_fan_mode
|
||||
self._current_operation = current_operation
|
||||
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
|
||||
self._current_fan_mode = current_fan_mode
|
||||
if self._topic[CONF_MODE_STATE_TOPIC] is None:
|
||||
self._current_operation = current_operation
|
||||
self._aux = aux
|
||||
self._current_swing_mode = current_swing_mode
|
||||
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None:
|
||||
self._current_swing_mode = current_swing_mode
|
||||
self._fan_list = fan_mode_list
|
||||
self._operation_list = mode_list
|
||||
self._swing_list = swing_mode_list
|
||||
self._target_temperature_step = 1
|
||||
self._target_temperature_step = temp_step
|
||||
self._send_if_off = send_if_off
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
|
@ -1,34 +1,23 @@
|
||||
"""
|
||||
Support for OpenTherm Gateway devices.
|
||||
Support for OpenTherm Gateway climate devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
For more details about this platform, please refer to the documentation at
|
||||
http://home-assistant.io/components/climate.opentherm_gw/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA,
|
||||
STATE_IDLE, STATE_HEAT,
|
||||
STATE_COOL,
|
||||
from homeassistant.components.climate import (ClimateDevice, STATE_IDLE,
|
||||
STATE_HEAT, STATE_COOL,
|
||||
SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import (ATTR_TEMPERATURE, CONF_DEVICE, CONF_NAME,
|
||||
PRECISION_HALVES, PRECISION_TENTHS,
|
||||
TEMP_CELSIUS, PRECISION_WHOLE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.opentherm_gw import (
|
||||
CONF_FLOOR_TEMP, CONF_PRECISION, DATA_DEVICE, DATA_GW_VARS,
|
||||
DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE)
|
||||
from homeassistant.const import (ATTR_TEMPERATURE, CONF_NAME, PRECISION_HALVES,
|
||||
PRECISION_TENTHS, PRECISION_WHOLE,
|
||||
TEMP_CELSIUS)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
REQUIREMENTS = ['pyotgw==0.1b0']
|
||||
|
||||
CONF_FLOOR_TEMP = "floor_temperature"
|
||||
CONF_PRECISION = 'precision'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DEVICE): cv.string,
|
||||
vol.Optional(CONF_NAME, default="OpenTherm Gateway"): cv.string,
|
||||
vol.Optional(CONF_PRECISION): vol.In([PRECISION_TENTHS, PRECISION_HALVES,
|
||||
PRECISION_WHOLE]),
|
||||
vol.Optional(CONF_FLOOR_TEMP, default=False): cv.boolean,
|
||||
})
|
||||
DEPENDENCIES = ['opentherm_gw']
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -37,19 +26,17 @@ _LOGGER = logging.getLogger(__name__)
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the opentherm_gw device."""
|
||||
gateway = OpenThermGateway(config)
|
||||
gateway = OpenThermGateway(hass, discovery_info)
|
||||
async_add_entities([gateway])
|
||||
|
||||
|
||||
class OpenThermGateway(ClimateDevice):
|
||||
"""Representation of a climate device."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the sensor."""
|
||||
import pyotgw
|
||||
self.pyotgw = pyotgw
|
||||
self.gateway = self.pyotgw.pyotgw()
|
||||
self._device = config[CONF_DEVICE]
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize the device."""
|
||||
self._gateway = hass.data[DATA_OPENTHERM_GW][DATA_DEVICE]
|
||||
self._gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS]
|
||||
self.friendly_name = config.get(CONF_NAME)
|
||||
self.floor_temp = config.get(CONF_FLOOR_TEMP)
|
||||
self.temp_precision = config.get(CONF_PRECISION)
|
||||
@ -63,40 +50,38 @@ class OpenThermGateway(ClimateDevice):
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Connect to the OpenTherm Gateway device."""
|
||||
await self.gateway.connect(self.hass.loop, self._device)
|
||||
self.gateway.subscribe(self.receive_report)
|
||||
_LOGGER.debug("Connected to %s on %s", self.friendly_name,
|
||||
self._device)
|
||||
_LOGGER.debug("Added device %s", self.friendly_name)
|
||||
async_dispatcher_connect(self.hass, SIGNAL_OPENTHERM_GW_UPDATE,
|
||||
self.receive_report)
|
||||
|
||||
async def receive_report(self, status):
|
||||
"""Receive and handle a new report from the Gateway."""
|
||||
_LOGGER.debug("Received report: %s", status)
|
||||
ch_active = status.get(self.pyotgw.DATA_SLAVE_CH_ACTIVE)
|
||||
flame_on = status.get(self.pyotgw.DATA_SLAVE_FLAME_ON)
|
||||
cooling_active = status.get(self.pyotgw.DATA_SLAVE_COOLING_ACTIVE)
|
||||
ch_active = status.get(self._gw_vars.DATA_SLAVE_CH_ACTIVE)
|
||||
flame_on = status.get(self._gw_vars.DATA_SLAVE_FLAME_ON)
|
||||
cooling_active = status.get(self._gw_vars.DATA_SLAVE_COOLING_ACTIVE)
|
||||
if ch_active and flame_on:
|
||||
self._current_operation = STATE_HEAT
|
||||
elif cooling_active:
|
||||
self._current_operation = STATE_COOL
|
||||
else:
|
||||
self._current_operation = STATE_IDLE
|
||||
self._current_temperature = status.get(self.pyotgw.DATA_ROOM_TEMP)
|
||||
self._current_temperature = status.get(self._gw_vars.DATA_ROOM_TEMP)
|
||||
|
||||
temp = status.get(self.pyotgw.DATA_ROOM_SETPOINT_OVRD)
|
||||
temp = status.get(self._gw_vars.DATA_ROOM_SETPOINT_OVRD)
|
||||
if temp is None:
|
||||
temp = status.get(self.pyotgw.DATA_ROOM_SETPOINT)
|
||||
temp = status.get(self._gw_vars.DATA_ROOM_SETPOINT)
|
||||
self._target_temperature = temp
|
||||
|
||||
# GPIO mode 5: 0 == Away
|
||||
# GPIO mode 6: 1 == Away
|
||||
gpio_a_state = status.get(self.pyotgw.OTGW_GPIO_A)
|
||||
gpio_a_state = status.get(self._gw_vars.OTGW_GPIO_A)
|
||||
if gpio_a_state == 5:
|
||||
self._away_mode_a = 0
|
||||
elif gpio_a_state == 6:
|
||||
self._away_mode_a = 1
|
||||
else:
|
||||
self._away_mode_a = None
|
||||
gpio_b_state = status.get(self.pyotgw.OTGW_GPIO_B)
|
||||
gpio_b_state = status.get(self._gw_vars.OTGW_GPIO_B)
|
||||
if gpio_b_state == 5:
|
||||
self._away_mode_b = 0
|
||||
elif gpio_b_state == 6:
|
||||
@ -104,11 +89,11 @@ class OpenThermGateway(ClimateDevice):
|
||||
else:
|
||||
self._away_mode_b = None
|
||||
if self._away_mode_a is not None:
|
||||
self._away_state_a = (status.get(self.pyotgw.OTGW_GPIO_A_STATE) ==
|
||||
self._away_mode_a)
|
||||
self._away_state_a = (status.get(
|
||||
self._gw_vars.OTGW_GPIO_A_STATE) == self._away_mode_a)
|
||||
if self._away_mode_b is not None:
|
||||
self._away_state_b = (status.get(self.pyotgw.OTGW_GPIO_B_STATE) ==
|
||||
self._away_mode_b)
|
||||
self._away_state_b = (status.get(
|
||||
self._gw_vars.OTGW_GPIO_B_STATE) == self._away_mode_b)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
@ -170,7 +155,7 @@ class OpenThermGateway(ClimateDevice):
|
||||
"""Set new target temperature."""
|
||||
if ATTR_TEMPERATURE in kwargs:
|
||||
temp = float(kwargs[ATTR_TEMPERATURE])
|
||||
self._target_temperature = await self.gateway.set_target_temp(
|
||||
self._target_temperature = await self._gateway.set_target_temp(
|
||||
temp)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
|
@ -123,26 +123,6 @@ nuheat_resume_program:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
|
||||
econet_add_vacation:
|
||||
description: Add a vacation to your water heater.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.water_heater'
|
||||
start_date:
|
||||
description: The timestamp of when the vacation should start. (Optional, defaults to now)
|
||||
example: 1513186320
|
||||
end_date:
|
||||
description: The timestamp of when the vacation should end.
|
||||
example: 1513445520
|
||||
|
||||
econet_delete_vacation:
|
||||
description: Delete your existing vacation from your water heater.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.water_heater'
|
||||
|
||||
sensibo_assume_state:
|
||||
description: Set Sensibo device to external state.
|
||||
fields:
|
||||
|
@ -8,7 +8,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.toon/
|
||||
"""
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_TEMPERATURE, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_PERFORMANCE,
|
||||
ATTR_TEMPERATURE, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_AUTO,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice)
|
||||
import homeassistant.components.toon as toon_main
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
@ -34,7 +34,7 @@ class ThermostatDevice(ClimateDevice):
|
||||
self._temperature = None
|
||||
self._setpoint = None
|
||||
self._operation_list = [
|
||||
STATE_PERFORMANCE,
|
||||
STATE_AUTO,
|
||||
STATE_HEAT,
|
||||
STATE_ECO,
|
||||
STATE_COOL,
|
||||
@ -84,7 +84,7 @@ class ThermostatDevice(ClimateDevice):
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new operation mode."""
|
||||
toonlib_values = {
|
||||
STATE_PERFORMANCE: 'Comfort',
|
||||
STATE_AUTO: 'Comfort',
|
||||
STATE_HEAT: 'Home',
|
||||
STATE_ECO: 'Away',
|
||||
STATE_COOL: 'Sleep',
|
||||
|
@ -7,8 +7,7 @@ https://home-assistant.io/components/climate.tuya/
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_TEMPERATURE, ENTITY_ID_FORMAT, STATE_AUTO, STATE_COOL, STATE_ECO,
|
||||
STATE_ELECTRIC, STATE_FAN_ONLY, STATE_GAS, STATE_HEAT, STATE_HEAT_PUMP,
|
||||
STATE_HIGH_DEMAND, STATE_PERFORMANCE, SUPPORT_FAN_MODE, SUPPORT_ON_OFF,
|
||||
STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice)
|
||||
from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH
|
||||
from homeassistant.components.tuya import DATA_TUYA, TuyaDevice
|
||||
@ -23,13 +22,8 @@ HA_STATE_TO_TUYA = {
|
||||
STATE_AUTO: 'auto',
|
||||
STATE_COOL: 'cold',
|
||||
STATE_ECO: 'eco',
|
||||
STATE_ELECTRIC: 'electric',
|
||||
STATE_FAN_ONLY: 'wind',
|
||||
STATE_GAS: 'gas',
|
||||
STATE_HEAT: 'hot',
|
||||
STATE_HEAT_PUMP: 'heat_pump',
|
||||
STATE_HIGH_DEMAND: 'high_demand',
|
||||
STATE_PERFORMANCE: 'performance',
|
||||
}
|
||||
|
||||
TUYA_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_TUYA.items()}
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
Support for Wink thermostats, Air Conditioners, and Water Heaters.
|
||||
Support for Wink thermostats and Air Conditioners.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.wink/
|
||||
@ -8,9 +8,9 @@ import logging
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_HUMIDITY, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TEMPERATURE, STATE_AUTO, STATE_COOL, STATE_ECO, STATE_ELECTRIC,
|
||||
STATE_FAN_ONLY, STATE_GAS, STATE_HEAT, STATE_HEAT_PUMP, STATE_HIGH_DEMAND,
|
||||
STATE_PERFORMANCE, SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE,
|
||||
ATTR_TEMPERATURE, STATE_AUTO, STATE_COOL, STATE_ECO,
|
||||
STATE_FAN_ONLY, STATE_HEAT, SUPPORT_AUX_HEAT,
|
||||
SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
|
||||
ClimateDevice)
|
||||
@ -24,11 +24,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ATTR_ECO_TARGET = 'eco_target'
|
||||
ATTR_EXTERNAL_TEMPERATURE = 'external_temperature'
|
||||
ATTR_OCCUPIED = 'occupied'
|
||||
ATTR_RHEEM_TYPE = 'rheem_type'
|
||||
ATTR_SCHEDULE_ENABLED = 'schedule_enabled'
|
||||
ATTR_SMART_TEMPERATURE = 'smart_temperature'
|
||||
ATTR_TOTAL_CONSUMPTION = 'total_consumption'
|
||||
ATTR_VACATION_MODE = 'vacation_mode'
|
||||
ATTR_HEAT_ON = 'heat_on'
|
||||
ATTR_COOL_ON = 'cool_on'
|
||||
|
||||
@ -42,14 +40,9 @@ HA_STATE_TO_WINK = {
|
||||
STATE_AUTO: 'auto',
|
||||
STATE_COOL: 'cool_only',
|
||||
STATE_ECO: 'eco',
|
||||
STATE_ELECTRIC: 'electric_only',
|
||||
STATE_FAN_ONLY: 'fan_only',
|
||||
STATE_GAS: 'gas',
|
||||
STATE_HEAT: 'heat_only',
|
||||
STATE_HEAT_PUMP: 'heat_pump',
|
||||
STATE_HIGH_DEMAND: 'high_demand',
|
||||
STATE_OFF: 'off',
|
||||
STATE_PERFORMANCE: 'performance',
|
||||
}
|
||||
|
||||
WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()}
|
||||
@ -62,9 +55,6 @@ SUPPORT_FLAGS_THERMOSTAT = (
|
||||
SUPPORT_FLAGS_AC = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_FAN_MODE)
|
||||
|
||||
SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_AWAY_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Wink climate devices."""
|
||||
@ -77,10 +67,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
_id = climate.object_id() + climate.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_entities([WinkAC(climate, hass)])
|
||||
for water_heater in pywink.get_water_heaters():
|
||||
_id = water_heater.object_id() + water_heater.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_entities([WinkWaterHeater(water_heater, hass)])
|
||||
|
||||
|
||||
class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
@ -504,93 +490,3 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
elif fan_mode == SPEED_HIGH:
|
||||
speed = 1.0
|
||||
self.wink.set_ac_fan_speed(speed)
|
||||
|
||||
|
||||
class WinkWaterHeater(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink water heater."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS_HEATER
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
# The Wink API always returns temp in Celsius
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the optional device state attributes."""
|
||||
data = {}
|
||||
data[ATTR_VACATION_MODE] = self.wink.vacation_mode_enabled()
|
||||
data[ATTR_RHEEM_TYPE] = self.wink.rheem_type()
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""
|
||||
Return current operation one of the following.
|
||||
|
||||
["eco", "performance", "heat_pump",
|
||||
"high_demand", "electric_only", "gas]
|
||||
"""
|
||||
if not self.wink.is_on():
|
||||
current_op = STATE_OFF
|
||||
else:
|
||||
current_op = WINK_STATE_TO_HA.get(self.wink.current_mode())
|
||||
if current_op is None:
|
||||
current_op = STATE_UNKNOWN
|
||||
return current_op
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
op_list = ['off']
|
||||
modes = self.wink.modes()
|
||||
for mode in modes:
|
||||
if mode == 'aux':
|
||||
continue
|
||||
ha_mode = WINK_STATE_TO_HA.get(mode)
|
||||
if ha_mode is not None:
|
||||
op_list.append(ha_mode)
|
||||
else:
|
||||
error = "Invalid operation mode mapping. " + mode + \
|
||||
" doesn't map. Please report this."
|
||||
_LOGGER.error(error)
|
||||
return op_list
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
self.wink.set_temperature(target_temp)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
op_mode_to_set = HA_STATE_TO_WINK.get(operation_mode)
|
||||
self.wink.set_operation_mode(op_mode_to_set)
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self.wink.current_set_point()
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
self.wink.set_vacation_mode(True)
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self.wink.set_vacation_mode(False)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self.wink.min_set_point()
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self.wink.max_set_point()
|
||||
|
@ -18,21 +18,23 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (async_dispatcher_connect,
|
||||
async_dispatcher_send)
|
||||
|
||||
REQUIREMENTS = ['zhong_hong_hvac==1.0.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_GATEWAY_ADDRRESS = 'gateway_address'
|
||||
|
||||
REQUIREMENTS = ['zhong_hong_hvac==1.0.9']
|
||||
DEFAULT_PORT = 9999
|
||||
DEFAULT_GATEWAY_ADDRRESS = 1
|
||||
|
||||
SIGNAL_DEVICE_ADDED = 'zhong_hong_device_added'
|
||||
SIGNAL_ZHONG_HONG_HUB_START = 'zhong_hong_hub_start'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST):
|
||||
cv.string,
|
||||
vol.Optional(CONF_PORT, default=9999):
|
||||
vol.Coerce(int),
|
||||
vol.Optional(CONF_GATEWAY_ADDRRESS, default=1):
|
||||
vol.Coerce(int),
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_GATEWAY_ADDRRESS, default=DEFAULT_GATEWAY_ADDRRESS):
|
||||
cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
|
@ -6,14 +6,15 @@ https://home-assistant.io/components/climate.zwave/
|
||||
"""
|
||||
# Because we do not compile openzwave on CI
|
||||
import logging
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.climate import (
|
||||
DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE)
|
||||
from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import
|
||||
ZWaveDeviceEntity, async_setup_platform)
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.const import (
|
||||
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -42,6 +43,22 @@ STATE_MAPPINGS = {
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Old method of setting up Z-Wave climate devices."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up Z-Wave Climate device from Config Entry."""
|
||||
@callback
|
||||
def async_add_climate(climate):
|
||||
"""Add Z-Wave Climate Device."""
|
||||
async_add_entities([climate])
|
||||
|
||||
async_dispatcher_connect(hass, 'zwave_new_climate', async_add_climate)
|
||||
|
||||
|
||||
def get_device(hass, values, **kwargs):
|
||||
"""Create Z-Wave entity device."""
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
|
@ -162,7 +162,7 @@ class Cloud:
|
||||
@property
|
||||
def subscription_expired(self):
|
||||
"""Return a boolean if the subscription has expired."""
|
||||
return dt_util.utcnow() > self.expiration_date + timedelta(days=3)
|
||||
return dt_util.utcnow() > self.expiration_date + timedelta(days=7)
|
||||
|
||||
@property
|
||||
def expiration_date(self):
|
||||
|
@ -113,6 +113,24 @@ def check_token(cloud):
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def renew_access_token(cloud):
|
||||
"""Renew access token."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(
|
||||
cloud,
|
||||
access_token=cloud.access_token,
|
||||
refresh_token=cloud.refresh_token)
|
||||
|
||||
try:
|
||||
cognito.renew_access_token()
|
||||
cloud.id_token = cognito.id_token
|
||||
cloud.access_token = cognito.access_token
|
||||
cloud.write_user_info()
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def _authenticate(cloud, email, password):
|
||||
"""Log in and return an authenticated Cognito instance."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.components import websocket_api
|
||||
|
||||
from . import auth_api
|
||||
from .const import DOMAIN, REQUEST_TIMEOUT
|
||||
from .iot import STATE_DISCONNECTED
|
||||
from .iot import STATE_DISCONNECTED, STATE_CONNECTED
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -249,13 +249,28 @@ async def websocket_subscription(hass, connection, msg):
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
response = await cloud.fetch_subscription_info()
|
||||
|
||||
if response.status == 200:
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], await response.json()))
|
||||
else:
|
||||
if response.status != 200:
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg['id'], 'request_failed', 'Failed to request subscription'))
|
||||
|
||||
data = await response.json()
|
||||
|
||||
# Check if a user is subscribed but local info is outdated
|
||||
# In that case, let's refresh and reconnect
|
||||
if data.get('provider') and cloud.iot.state != STATE_CONNECTED:
|
||||
_LOGGER.debug(
|
||||
"Found disconnected account with valid subscriotion, connecting")
|
||||
await hass.async_add_executor_job(
|
||||
auth_api.renew_access_token, cloud)
|
||||
|
||||
# Cancel reconnect in progress
|
||||
if cloud.iot.state != STATE_DISCONNECTED:
|
||||
await cloud.iot.disconnect()
|
||||
|
||||
hass.async_create_task(cloud.iot.connect())
|
||||
|
||||
connection.send_message(websocket_api.result_message(msg['id'], data))
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_update_prefs(hass, connection, msg):
|
||||
|
@ -92,6 +92,7 @@ def _user_info(user):
|
||||
'is_owner': user.is_owner,
|
||||
'is_active': user.is_active,
|
||||
'system_generated': user.system_generated,
|
||||
'group_ids': [group.id for group in user.groups],
|
||||
'credentials': [
|
||||
{
|
||||
'type': c.auth_provider_type,
|
||||
|
@ -5,10 +5,10 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.deconz/
|
||||
"""
|
||||
from homeassistant.components.deconz.const import (
|
||||
COVER_TYPES, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB,
|
||||
DECONZ_DOMAIN)
|
||||
COVER_TYPES, DAMPERS, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID,
|
||||
DATA_DECONZ_UNSUB, DECONZ_DOMAIN, WINDOW_COVERS)
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN,
|
||||
ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP,
|
||||
SUPPORT_SET_POSITION)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
||||
@ -16,6 +16,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['deconz']
|
||||
|
||||
ZIGBEE_SPEC = ['lumi.curtain']
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
@ -34,7 +36,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
entities = []
|
||||
for light in lights:
|
||||
if light.type in COVER_TYPES:
|
||||
entities.append(DeconzCover(light))
|
||||
if light.modelid in ZIGBEE_SPEC:
|
||||
entities.append(DeconzCoverZigbeeSpec(light))
|
||||
else:
|
||||
entities.append(DeconzCover(light))
|
||||
async_add_entities(entities, True)
|
||||
|
||||
hass.data[DATA_DECONZ_UNSUB].append(
|
||||
@ -49,7 +54,10 @@ class DeconzCover(CoverDevice):
|
||||
def __init__(self, cover):
|
||||
"""Set up cover and add update callback to get data from websocket."""
|
||||
self._cover = cover
|
||||
self._features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
|
||||
self._features = SUPPORT_OPEN
|
||||
self._features |= SUPPORT_CLOSE
|
||||
self._features |= SUPPORT_STOP
|
||||
self._features |= SUPPORT_SET_POSITION
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to covers events."""
|
||||
@ -91,7 +99,11 @@ class DeconzCover(CoverDevice):
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the cover."""
|
||||
return 'damper'
|
||||
if self._cover.type in DAMPERS:
|
||||
return 'damper'
|
||||
if self._cover.type in WINDOW_COVERS:
|
||||
return 'window'
|
||||
return None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
@ -127,6 +139,11 @@ class DeconzCover(CoverDevice):
|
||||
data = {ATTR_POSITION: 0}
|
||||
await self.async_set_cover_position(**data)
|
||||
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Stop cover."""
|
||||
data = {'bri_inc': 0}
|
||||
await self._cover.async_set_state(data)
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
@ -144,3 +161,26 @@ class DeconzCover(CoverDevice):
|
||||
'sw_version': self._cover.swversion,
|
||||
'via_hub': (DECONZ_DOMAIN, bridgeid),
|
||||
}
|
||||
|
||||
|
||||
class DeconzCoverZigbeeSpec(DeconzCover):
|
||||
"""Zigbee spec is the inverse of how deCONZ normally reports attributes."""
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of the cover."""
|
||||
return 100 - int(self._cover.brightness / 255 * 100)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self._cover.state
|
||||
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
position = kwargs[ATTR_POSITION]
|
||||
data = {'on': False}
|
||||
if position < 100:
|
||||
data['on'] = True
|
||||
data['bri'] = 255 - int(position / 100 * 255)
|
||||
await self._cover.async_set_state(data)
|
||||
|
@ -64,7 +64,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities):
|
||||
entities = []
|
||||
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
device = hass.data[DATA_KNX].xknx.devices[device_name]
|
||||
entities.append(KNXCover(hass, device))
|
||||
entities.append(KNXCover(device))
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@ -88,18 +88,15 @@ def async_add_entities_config(hass, config, async_add_entities):
|
||||
invert_angle=config.get(CONF_INVERT_ANGLE))
|
||||
|
||||
hass.data[DATA_KNX].xknx.devices.add(cover)
|
||||
async_add_entities([KNXCover(hass, cover)])
|
||||
async_add_entities([KNXCover(cover)])
|
||||
|
||||
|
||||
class KNXCover(CoverDevice):
|
||||
"""Representation of a KNX cover."""
|
||||
|
||||
def __init__(self, hass, device):
|
||||
def __init__(self, device):
|
||||
"""Initialize the cover."""
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
|
||||
self._unsubscribe_auto_updater = None
|
||||
|
||||
@callback
|
||||
@ -110,6 +107,10 @@ class KNXCover(CoverDevice):
|
||||
await self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Store register state change callback."""
|
||||
self.async_register_callbacks()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
|
@ -19,12 +19,12 @@ from homeassistant.components.cover import (
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN,
|
||||
STATE_CLOSED, STATE_UNKNOWN)
|
||||
STATE_CLOSED, STATE_UNKNOWN, CONF_DEVICE)
|
||||
from homeassistant.components.mqtt import (
|
||||
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
|
||||
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic,
|
||||
MqttAvailability, MqttDiscoveryUpdate)
|
||||
MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo)
|
||||
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@ -96,6 +96,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_TILT_INVERT_STATE,
|
||||
default=DEFAULT_TILT_INVERT_STATE): cv.boolean,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
@ -155,11 +156,13 @@ async def _async_setup_entity(hass, config, async_add_entities,
|
||||
config.get(CONF_POSITION_TOPIC),
|
||||
set_position_template,
|
||||
config.get(CONF_UNIQUE_ID),
|
||||
config.get(CONF_DEVICE),
|
||||
discovery_hash
|
||||
)])
|
||||
|
||||
|
||||
class MqttCover(MqttAvailability, MqttDiscoveryUpdate, CoverDevice):
|
||||
class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
||||
CoverDevice):
|
||||
"""Representation of a cover that can be controlled using MQTT."""
|
||||
|
||||
def __init__(self, name, state_topic, command_topic, availability_topic,
|
||||
@ -169,11 +172,13 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, CoverDevice):
|
||||
optimistic, value_template, tilt_open_position,
|
||||
tilt_closed_position, tilt_min, tilt_max, tilt_optimistic,
|
||||
tilt_invert, position_topic, set_position_template,
|
||||
unique_id: Optional[str], discovery_hash):
|
||||
unique_id: Optional[str], device_config: Optional[ConfigType],
|
||||
discovery_hash):
|
||||
"""Initialize the cover."""
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash)
|
||||
MqttEntityDeviceInfo.__init__(self, device_config)
|
||||
self._position = None
|
||||
self._state = None
|
||||
self._name = name
|
||||
|
@ -9,8 +9,9 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.rflink import (
|
||||
DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP,
|
||||
DEVICE_DEFAULTS_SCHEMA, EVENT_KEY_COMMAND, RflinkCommand)
|
||||
CONF_ALIASES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT,
|
||||
CONF_GROUP, CONF_GROUP_ALIASES, CONF_NOGROUP_ALIASES,
|
||||
CONF_SIGNAL_REPETITIONS, DEVICE_DEFAULTS_SCHEMA, RflinkCommand)
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -22,19 +23,6 @@ DEPENDENCIES = ['rflink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CONF_ALIASES = 'aliases'
|
||||
CONF_GROUP_ALIASES = 'group_aliases'
|
||||
CONF_GROUP = 'group'
|
||||
CONF_NOGROUP_ALIASES = 'nogroup_aliases'
|
||||
CONF_DEVICE_DEFAULTS = 'device_defaults'
|
||||
CONF_DEVICES = 'devices'
|
||||
CONF_AUTOMATIC_ADD = 'automatic_add'
|
||||
CONF_FIRE_EVENT = 'fire_event'
|
||||
CONF_IGNORE_DEVICES = 'ignore_devices'
|
||||
CONF_RECONNECT_INTERVAL = 'reconnect_interval'
|
||||
CONF_SIGNAL_REPETITIONS = 'signal_repetitions'
|
||||
CONF_WAIT_FOR_ACK = 'wait_for_ack'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})):
|
||||
DEVICE_DEFAULTS_SCHEMA,
|
||||
@ -55,33 +43,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def devices_from_config(domain_config, hass=None):
|
||||
def devices_from_config(domain_config):
|
||||
"""Parse configuration and add Rflink cover devices."""
|
||||
devices = []
|
||||
for device_id, config in domain_config[CONF_DEVICES].items():
|
||||
device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config)
|
||||
device = RflinkCover(device_id, hass, **device_config)
|
||||
device = RflinkCover(device_id, **device_config)
|
||||
devices.append(device)
|
||||
|
||||
# Register entity (and aliases) to listen to incoming rflink events
|
||||
# Device id and normal aliases respond to normal and group command
|
||||
hass.data[DATA_ENTITY_LOOKUP][
|
||||
EVENT_KEY_COMMAND][device_id].append(device)
|
||||
if config[CONF_GROUP]:
|
||||
hass.data[DATA_ENTITY_GROUP_LOOKUP][
|
||||
EVENT_KEY_COMMAND][device_id].append(device)
|
||||
for _id in config[CONF_ALIASES]:
|
||||
hass.data[DATA_ENTITY_LOOKUP][
|
||||
EVENT_KEY_COMMAND][_id].append(device)
|
||||
hass.data[DATA_ENTITY_GROUP_LOOKUP][
|
||||
EVENT_KEY_COMMAND][_id].append(device)
|
||||
return devices
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Rflink cover platform."""
|
||||
async_add_entities(devices_from_config(config, hass))
|
||||
async_add_entities(devices_from_config(config))
|
||||
|
||||
|
||||
class RflinkCover(RflinkCommand, CoverDevice):
|
||||
|
@ -1,103 +0,0 @@
|
||||
"""
|
||||
Ryobi platform for the cover component.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/cover.ryobi_gdo/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE)
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, STATE_UNKNOWN, STATE_CLOSED)
|
||||
|
||||
REQUIREMENTS = ['py_ryobi_gdo==0.0.10']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DEVICE_ID = 'device_id'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
})
|
||||
|
||||
SUPPORTED_FEATURES = (SUPPORT_OPEN | SUPPORT_CLOSE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Ryobi covers."""
|
||||
from py_ryobi_gdo import RyobiGDO as ryobi_door
|
||||
covers = []
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
devices = config.get(CONF_DEVICE_ID)
|
||||
|
||||
for device_id in devices:
|
||||
my_door = ryobi_door(username, password, device_id)
|
||||
_LOGGER.debug("Getting the API key")
|
||||
if my_door.get_api_key() is False:
|
||||
_LOGGER.error("Wrong credentials, no API key retrieved")
|
||||
return
|
||||
_LOGGER.debug("Checking if the device ID is present")
|
||||
if my_door.check_device_id() is False:
|
||||
_LOGGER.error("%s not in your device list", device_id)
|
||||
return
|
||||
_LOGGER.debug("Adding device %s to covers", device_id)
|
||||
covers.append(RyobiCover(hass, my_door))
|
||||
if covers:
|
||||
_LOGGER.debug("Adding covers")
|
||||
add_entities(covers, True)
|
||||
|
||||
|
||||
class RyobiCover(CoverDevice):
|
||||
"""Representation of a ryobi cover."""
|
||||
|
||||
def __init__(self, hass, ryobi_door):
|
||||
"""Initialize the cover."""
|
||||
self.ryobi_door = ryobi_door
|
||||
self._name = 'ryobi_gdo_{}'.format(ryobi_door.get_device_id())
|
||||
self._door_state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self._door_state == STATE_UNKNOWN:
|
||||
return False
|
||||
return self._door_state == STATE_CLOSED
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'garage'
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORTED_FEATURES
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
_LOGGER.debug("Closing garage door")
|
||||
self.ryobi_door.close_device()
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
_LOGGER.debug("Opening garage door")
|
||||
self.ryobi_door.open_device()
|
||||
|
||||
def update(self):
|
||||
"""Update status from the door."""
|
||||
_LOGGER.debug("Updating RyobiGDO status")
|
||||
self.ryobi_door.update()
|
||||
self._door_state = self.ryobi_door.get_door_status()
|
@ -278,9 +278,10 @@ class CoverTemplate(CoverDevice):
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Move the cover up."""
|
||||
if self._open_script:
|
||||
await self._open_script.async_run()
|
||||
await self._open_script.async_run(context=self._context)
|
||||
elif self._position_script:
|
||||
await self._position_script.async_run({"position": 100})
|
||||
await self._position_script.async_run(
|
||||
{"position": 100}, context=self._context)
|
||||
if self._optimistic:
|
||||
self._position = 100
|
||||
self.async_schedule_update_ha_state()
|
||||
@ -288,9 +289,10 @@ class CoverTemplate(CoverDevice):
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
if self._close_script:
|
||||
await self._close_script.async_run()
|
||||
await self._close_script.async_run(context=self._context)
|
||||
elif self._position_script:
|
||||
await self._position_script.async_run({"position": 0})
|
||||
await self._position_script.async_run(
|
||||
{"position": 0}, context=self._context)
|
||||
if self._optimistic:
|
||||
self._position = 0
|
||||
self.async_schedule_update_ha_state()
|
||||
@ -298,20 +300,21 @@ class CoverTemplate(CoverDevice):
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Fire the stop action."""
|
||||
if self._stop_script:
|
||||
await self._stop_script.async_run()
|
||||
await self._stop_script.async_run(context=self._context)
|
||||
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Set cover position."""
|
||||
self._position = kwargs[ATTR_POSITION]
|
||||
await self._position_script.async_run(
|
||||
{"position": self._position})
|
||||
{"position": self._position}, context=self._context)
|
||||
if self._optimistic:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_open_cover_tilt(self, **kwargs):
|
||||
"""Tilt the cover open."""
|
||||
self._tilt_value = 100
|
||||
await self._tilt_script.async_run({"tilt": self._tilt_value})
|
||||
await self._tilt_script.async_run(
|
||||
{"tilt": self._tilt_value}, context=self._context)
|
||||
if self._tilt_optimistic:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@ -319,14 +322,15 @@ class CoverTemplate(CoverDevice):
|
||||
"""Tilt the cover closed."""
|
||||
self._tilt_value = 0
|
||||
await self._tilt_script.async_run(
|
||||
{"tilt": self._tilt_value})
|
||||
{"tilt": self._tilt_value}, context=self._context)
|
||||
if self._tilt_optimistic:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
self._tilt_value = kwargs[ATTR_TILT_POSITION]
|
||||
await self._tilt_script.async_run({"tilt": self._tilt_value})
|
||||
await self._tilt_script.async_run(
|
||||
{"tilt": self._tilt_value}, context=self._context)
|
||||
if self._tilt_optimistic:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
|
@ -4,21 +4,37 @@ Support for Z-Wave cover components.
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/cover.zwave/
|
||||
"""
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.cover import (
|
||||
DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE, ATTR_POSITION)
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import
|
||||
ZWaveDeviceEntity, async_setup_platform, workaround)
|
||||
from homeassistant.components.zwave import (
|
||||
ZWaveDeviceEntity, workaround)
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Old method of setting up Z-Wave covers."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up Z-Wave Cover from Config Entry."""
|
||||
@callback
|
||||
def async_add_cover(cover):
|
||||
"""Add Z-Wave Cover."""
|
||||
async_add_entities([cover])
|
||||
|
||||
async_dispatcher_connect(hass, 'zwave_new_cover', async_add_cover)
|
||||
|
||||
|
||||
def get_device(hass, values, node_config, **kwargs):
|
||||
"""Create Z-Wave entity device."""
|
||||
invert_buttons = node_config.get(zwave.CONF_INVERT_OPENCLOSE_BUTTONS)
|
||||
|
@ -28,6 +28,6 @@
|
||||
"title": "Op\u00e7\u00f5es extra de configura\u00e7\u00e3o para deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
"title": "Gateway Zigbee deCONZ"
|
||||
}
|
||||
}
|
@ -16,7 +16,9 @@ CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
|
||||
ATTR_DARK = 'dark'
|
||||
ATTR_ON = 'on'
|
||||
|
||||
COVER_TYPES = ["Level controllable output"]
|
||||
DAMPERS = ["Level controllable output"]
|
||||
WINDOW_COVERS = ["Window covering device"]
|
||||
COVER_TYPES = DAMPERS + WINDOW_COVERS
|
||||
|
||||
POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"]
|
||||
SIRENS = ["Warning device"]
|
||||
|
@ -10,20 +10,26 @@ import re
|
||||
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
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}')
|
||||
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
|
||||
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_VERIFY_SSL = True
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
@ -39,7 +45,9 @@ class DdWrtDeviceScanner(DeviceScanner):
|
||||
"""This class queries a wireless router running DD-WRT firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
"""Initialize the DD-WRT scanner."""
|
||||
self.protocol = 'https' if config[CONF_SSL] else 'http'
|
||||
self.verify_ssl = config[CONF_VERIFY_SSL]
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
@ -48,7 +56,8 @@ class DdWrtDeviceScanner(DeviceScanner):
|
||||
self.mac2name = {}
|
||||
|
||||
# Test the router is accessible
|
||||
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
|
||||
url = '{}://{}/Status_Wireless.live.asp'.format(
|
||||
self.protocol, self.host)
|
||||
data = self.get_ddwrt_data(url)
|
||||
if not data:
|
||||
raise ConnectionError('Cannot connect to DD-Wrt router')
|
||||
@ -63,7 +72,8 @@ class DdWrtDeviceScanner(DeviceScanner):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
# If not initialised and not already scanned and not found.
|
||||
if device not in self.mac2name:
|
||||
url = 'http://{}/Status_Lan.live.asp'.format(self.host)
|
||||
url = '{}://{}/Status_Lan.live.asp'.format(
|
||||
self.protocol, self.host)
|
||||
data = self.get_ddwrt_data(url)
|
||||
|
||||
if not data:
|
||||
@ -98,7 +108,8 @@ class DdWrtDeviceScanner(DeviceScanner):
|
||||
"""
|
||||
_LOGGER.info("Checking ARP")
|
||||
|
||||
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
|
||||
url = '{}://{}/Status_Wireless.live.asp'.format(
|
||||
self.protocol, self.host)
|
||||
data = self.get_ddwrt_data(url)
|
||||
|
||||
if not data:
|
||||
@ -125,7 +136,8 @@ class DdWrtDeviceScanner(DeviceScanner):
|
||||
"""Retrieve data from DD-WRT and return parsed result."""
|
||||
try:
|
||||
response = requests.get(
|
||||
url, auth=(self.username, self.password), timeout=4)
|
||||
url, auth=(self.username, self.password),
|
||||
timeout=4, verify=self.verify_ssl)
|
||||
except requests.exceptions.Timeout:
|
||||
_LOGGER.exception("Connection to the router timed out")
|
||||
return
|
||||
@ -142,5 +154,4 @@ class DdWrtDeviceScanner(DeviceScanner):
|
||||
def _parse_ddwrt_response(data_str):
|
||||
"""Parse the DD-WRT data format."""
|
||||
return {
|
||||
key: val for key, val in _DDWRT_DATA_REGEX
|
||||
.findall(data_str)}
|
||||
key: val for key, val in _DDWRT_DATA_REGEX.findall(data_str)}
|
||||
|
@ -43,8 +43,7 @@ class FritzBoxScanner(DeviceScanner):
|
||||
self.password = config[CONF_PASSWORD]
|
||||
self.success_init = True
|
||||
|
||||
# pylint: disable=import-error
|
||||
import fritzconnection as fc
|
||||
import fritzconnection as fc # pylint: disable=import-error
|
||||
|
||||
# Establish a connection to the FRITZ!Box.
|
||||
try:
|
||||
|
@ -19,7 +19,7 @@ from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import slugify, dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['locationsharinglib==3.0.3']
|
||||
REQUIREMENTS = ['locationsharinglib==3.0.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL,
|
||||
CONF_DEVICES, CONF_EXCLUDE)
|
||||
|
||||
REQUIREMENTS = ['pynetgear==0.4.2']
|
||||
REQUIREMENTS = ['pynetgear==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
69
homeassistant/components/device_tracker/quantum_gateway.py
Normal file
69
homeassistant/components/device_tracker/quantum_gateway.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""
|
||||
Support for Verizon FiOS Quantum Gateways.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.quantum_gateway/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA,
|
||||
DeviceScanner)
|
||||
from homeassistant.const import (CONF_HOST, CONF_PASSWORD)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['quantum-gateway==0.0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = 'myfiosgateway.com'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a Quantum Gateway scanner."""
|
||||
scanner = QuantumGatewayDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class QuantumGatewayDeviceScanner(DeviceScanner):
|
||||
"""This class queries a Quantum Gateway."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
from quantum_gateway import QuantumGatewayScanner
|
||||
|
||||
self.host = config[CONF_HOST]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
_LOGGER.debug('Initializing')
|
||||
|
||||
try:
|
||||
self.quantum = QuantumGatewayScanner(self.host, self.password)
|
||||
self.success_init = self.quantum.success_init
|
||||
except RequestException:
|
||||
self.success_init = False
|
||||
_LOGGER.error("Unable to connect to gateway. Check host.")
|
||||
|
||||
if not self.success_init:
|
||||
_LOGGER.error("Unable to login to gateway. Check password and "
|
||||
"host.")
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list of found MACs."""
|
||||
connected_devices = []
|
||||
try:
|
||||
connected_devices = self.quantum.scan_devices()
|
||||
except RequestException:
|
||||
_LOGGER.error("Unable to scan devices. Check connection to router")
|
||||
return connected_devices
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
return self.quantum.get_device_name(device)
|
@ -86,10 +86,9 @@ class UnifiDeviceScanner(DeviceScanner):
|
||||
|
||||
def _disconnect(self):
|
||||
"""Disconnect the current SSH connection."""
|
||||
# pylint: disable=broad-except
|
||||
try:
|
||||
self.ssh.logout()
|
||||
except Exception:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
finally:
|
||||
self.ssh = None
|
||||
|
@ -8,10 +8,12 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_TOKEN
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA,
|
||||
DeviceScanner)
|
||||
from homeassistant.const import (CONF_HOST, CONF_TOKEN)
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -20,8 +22,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Return a Xiaomi MiIO device scanner."""
|
||||
@ -56,7 +56,7 @@ class XiaomiMiioDeviceScanner(DeviceScanner):
|
||||
self.device = device
|
||||
|
||||
async def async_scan_devices(self):
|
||||
"""Scan for devices and return a list containing found device ids."""
|
||||
"""Scan for devices and return a list containing found device IDs."""
|
||||
from miio import DeviceException
|
||||
|
||||
devices = []
|
||||
@ -68,7 +68,7 @@ class XiaomiMiioDeviceScanner(DeviceScanner):
|
||||
devices.append(device['mac'])
|
||||
|
||||
except DeviceException as ex:
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
_LOGGER.error("Unable to fetch the state: %s", ex)
|
||||
|
||||
return devices
|
||||
|
||||
|
@ -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==2.1.0']
|
||||
REQUIREMENTS = ['netdisco==2.2.0']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
@ -43,6 +43,7 @@ SERVICE_DAIKIN = 'daikin'
|
||||
SERVICE_SABNZBD = 'sabnzbd'
|
||||
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
|
||||
SERVICE_HOMEKIT = 'homekit'
|
||||
SERVICE_OCTOPRINT = 'octoprint'
|
||||
|
||||
CONFIG_ENTRY_HANDLERS = {
|
||||
SERVICE_DECONZ: 'deconz',
|
||||
@ -67,6 +68,7 @@ SERVICE_HANDLERS = {
|
||||
SERVICE_SABNZBD: ('sabnzbd', None),
|
||||
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
|
||||
SERVICE_KONNECTED: ('konnected', None),
|
||||
SERVICE_OCTOPRINT: ('octoprint', None),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
'plex_mediaserver': ('media_player', 'plex'),
|
||||
'roku': ('media_player', 'roku'),
|
||||
@ -84,6 +86,7 @@ SERVICE_HANDLERS = {
|
||||
'songpal': ('media_player', 'songpal'),
|
||||
'kodi': ('media_player', 'kodi'),
|
||||
'volumio': ('media_player', 'volumio'),
|
||||
'lg_smart_device': ('media_player', 'lg_soundbar'),
|
||||
'nanoleaf_aurora': ('light', 'nanoleaf_aurora'),
|
||||
'freebox': ('device_tracker', 'freebox'),
|
||||
}
|
||||
|
@ -102,5 +102,6 @@ def setup(hass, config):
|
||||
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, "fan", DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, "vacuum", DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, "climate", DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
232
homeassistant/components/elkm1/__init__.py
Normal file
232
homeassistant/components/elkm1/__init__.py
Normal file
@ -0,0 +1,232 @@
|
||||
"""
|
||||
Support the ElkM1 Gold and ElkM1 EZ8 alarm / integration panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/elkm1/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import (
|
||||
CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE, CONF_PASSWORD,
|
||||
CONF_TEMPERATURE_UNIT, CONF_USERNAME, TEMP_FAHRENHEIT)
|
||||
from homeassistant.core import HomeAssistant, callback # noqa
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import ConfigType # noqa
|
||||
|
||||
DOMAIN = "elkm1"
|
||||
|
||||
REQUIREMENTS = ['elkm1-lib==0.7.10']
|
||||
|
||||
CONF_AREA = 'area'
|
||||
CONF_COUNTER = 'counter'
|
||||
CONF_KEYPAD = 'keypad'
|
||||
CONF_OUTPUT = 'output'
|
||||
CONF_SETTING = 'setting'
|
||||
CONF_TASK = 'task'
|
||||
CONF_THERMOSTAT = 'thermostat'
|
||||
CONF_PLC = 'plc'
|
||||
CONF_ZONE = 'zone'
|
||||
CONF_ENABLED = 'enabled'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORTED_DOMAINS = ['alarm_control_panel', 'climate', 'light', 'scene',
|
||||
'sensor', 'switch']
|
||||
|
||||
SPEAK_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required('number'):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=999))
|
||||
})
|
||||
|
||||
|
||||
def _host_validator(config):
|
||||
"""Validate that a host is properly configured."""
|
||||
if config[CONF_HOST].startswith('elks://'):
|
||||
if CONF_USERNAME not in config or CONF_PASSWORD not in config:
|
||||
raise vol.Invalid("Specify username and password for elks://")
|
||||
elif not config[CONF_HOST].startswith('elk://') and not config[
|
||||
CONF_HOST].startswith('serial://'):
|
||||
raise vol.Invalid("Invalid host URL")
|
||||
return config
|
||||
|
||||
|
||||
def _elk_range_validator(rng):
|
||||
def _housecode_to_int(val):
|
||||
match = re.search(r'^([a-p])(0[1-9]|1[0-6]|[1-9])$', val.lower())
|
||||
if match:
|
||||
return (ord(match.group(1)) - ord('a')) * 16 + int(match.group(2))
|
||||
raise vol.Invalid("Invalid range")
|
||||
|
||||
def _elk_value(val):
|
||||
return int(val) if val.isdigit() else _housecode_to_int(val)
|
||||
|
||||
vals = [s.strip() for s in str(rng).split('-')]
|
||||
start = _elk_value(vals[0])
|
||||
end = start if len(vals) == 1 else _elk_value(vals[1])
|
||||
return (start, end)
|
||||
|
||||
|
||||
CONFIG_SCHEMA_SUBDOMAIN = vol.Schema({
|
||||
vol.Optional(CONF_ENABLED, default=True): cv.boolean,
|
||||
vol.Optional(CONF_INCLUDE, default=[]): [_elk_range_validator],
|
||||
vol.Optional(CONF_EXCLUDE, default=[]): [_elk_range_validator],
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=''): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=''): cv.string,
|
||||
vol.Optional(CONF_TEMPERATURE_UNIT, default=TEMP_FAHRENHEIT):
|
||||
cv.temperature_unit,
|
||||
vol.Optional(CONF_AREA): CONFIG_SCHEMA_SUBDOMAIN,
|
||||
vol.Optional(CONF_COUNTER): CONFIG_SCHEMA_SUBDOMAIN,
|
||||
vol.Optional(CONF_KEYPAD): CONFIG_SCHEMA_SUBDOMAIN,
|
||||
vol.Optional(CONF_OUTPUT): CONFIG_SCHEMA_SUBDOMAIN,
|
||||
vol.Optional(CONF_PLC): CONFIG_SCHEMA_SUBDOMAIN,
|
||||
vol.Optional(CONF_SETTING): CONFIG_SCHEMA_SUBDOMAIN,
|
||||
vol.Optional(CONF_TASK): CONFIG_SCHEMA_SUBDOMAIN,
|
||||
vol.Optional(CONF_THERMOSTAT): CONFIG_SCHEMA_SUBDOMAIN,
|
||||
vol.Optional(CONF_ZONE): CONFIG_SCHEMA_SUBDOMAIN,
|
||||
},
|
||||
_host_validator,
|
||||
)
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
|
||||
"""Set up the Elk M1 platform."""
|
||||
from elkm1_lib.const import Max
|
||||
import elkm1_lib as elkm1
|
||||
|
||||
configs = {
|
||||
CONF_AREA: Max.AREAS.value,
|
||||
CONF_COUNTER: Max.COUNTERS.value,
|
||||
CONF_KEYPAD: Max.KEYPADS.value,
|
||||
CONF_OUTPUT: Max.OUTPUTS.value,
|
||||
CONF_PLC: Max.LIGHTS.value,
|
||||
CONF_SETTING: Max.SETTINGS.value,
|
||||
CONF_TASK: Max.TASKS.value,
|
||||
CONF_THERMOSTAT: Max.THERMOSTATS.value,
|
||||
CONF_ZONE: Max.ZONES.value,
|
||||
}
|
||||
|
||||
def _included(ranges, set_to, values):
|
||||
for rng in ranges:
|
||||
if not rng[0] <= rng[1] <= len(values):
|
||||
raise vol.Invalid("Invalid range {}".format(rng))
|
||||
values[rng[0]-1:rng[1]] = [set_to] * (rng[1] - rng[0] + 1)
|
||||
|
||||
conf = hass_config[DOMAIN]
|
||||
config = {'temperature_unit': conf[CONF_TEMPERATURE_UNIT]}
|
||||
config['panel'] = {'enabled': True, 'included': [True]}
|
||||
|
||||
for item, max_ in configs.items():
|
||||
config[item] = {'enabled': conf[item][CONF_ENABLED],
|
||||
'included': [not conf[item]['include']] * max_}
|
||||
try:
|
||||
_included(conf[item]['include'], True, config[item]['included'])
|
||||
_included(conf[item]['exclude'], False, config[item]['included'])
|
||||
except (ValueError, vol.Invalid) as err:
|
||||
_LOGGER.error("Config item: %s; %s", item, err)
|
||||
return False
|
||||
|
||||
elk = elkm1.Elk({'url': conf[CONF_HOST], 'userid': conf[CONF_USERNAME],
|
||||
'password': conf[CONF_PASSWORD]})
|
||||
elk.connect()
|
||||
|
||||
_create_elk_services(hass, elk)
|
||||
|
||||
hass.data[DOMAIN] = {'elk': elk, 'config': config, 'keypads': {}}
|
||||
for component in SUPPORTED_DOMAINS:
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(hass, component, DOMAIN, {}))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _create_elk_services(hass, elk):
|
||||
def _speak_word_service(service):
|
||||
elk.panel.speak_word(service.data.get('number'))
|
||||
|
||||
def _speak_phrase_service(service):
|
||||
elk.panel.speak_phrase(service.data.get('number'))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, 'speak_word', _speak_word_service, SPEAK_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, 'speak_phrase', _speak_phrase_service, SPEAK_SERVICE_SCHEMA)
|
||||
|
||||
|
||||
def create_elk_entities(hass, elk_elements, element_type, class_, entities):
|
||||
"""Create the ElkM1 devices of a particular class."""
|
||||
elk_data = hass.data[DOMAIN]
|
||||
if elk_data['config'][element_type]['enabled']:
|
||||
elk = elk_data['elk']
|
||||
for element in elk_elements:
|
||||
if elk_data['config'][element_type]['included'][element.index]:
|
||||
entities.append(class_(element, elk, elk_data))
|
||||
return entities
|
||||
|
||||
|
||||
class ElkEntity(Entity):
|
||||
"""Base class for all Elk entities."""
|
||||
|
||||
def __init__(self, element, elk, elk_data):
|
||||
"""Initialize the base of all Elk devices."""
|
||||
self._elk = elk
|
||||
self._element = element
|
||||
self._temperature_unit = elk_data['config']['temperature_unit']
|
||||
self._unique_id = 'elkm1_{}'.format(
|
||||
self._element.default_name('_').lower())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name of the element."""
|
||||
return self._element.name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique id of the element."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Don't poll this device."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the default attributes of the element."""
|
||||
return {**self._element.as_dict(), **self.initial_attrs()}
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Is the entity available to be updated."""
|
||||
return self._elk.is_connected()
|
||||
|
||||
def initial_attrs(self):
|
||||
"""Return the underlying element's attributes as a dict."""
|
||||
attrs = {}
|
||||
attrs['index'] = self._element.index + 1
|
||||
return attrs
|
||||
|
||||
def _element_changed(self, element, changeset):
|
||||
pass
|
||||
|
||||
@callback
|
||||
def _element_callback(self, element, changeset):
|
||||
"""Handle callback from an Elk element that has changed."""
|
||||
self._element_changed(element, changeset)
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callback for ElkM1 changes and update entity state."""
|
||||
self._element.add_callback(self._element_callback)
|
||||
self._element_callback(self._element, {})
|
12
homeassistant/components/elkm1/services.yaml
Normal file
12
homeassistant/components/elkm1/services.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
speak_word:
|
||||
description: Speak a word. See list of words in ElkM1 ASCII Protocol documentation.
|
||||
fields:
|
||||
number:
|
||||
description: Word number to speak.
|
||||
example: 142
|
||||
speak_phrase:
|
||||
description: Speak a phrase. See list of phrases in ElkM1 ASCII Protocol documentation.
|
||||
fields:
|
||||
number:
|
||||
description: Phrase number to speak.
|
||||
example: 42
|
@ -10,20 +10,23 @@ from typing import Optional
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.components import fan, mqtt
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF,
|
||||
CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON)
|
||||
CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_DEVICE)
|
||||
from homeassistant.components.mqtt import (
|
||||
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
|
||||
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate)
|
||||
CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate,
|
||||
MqttEntityDeviceInfo)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
|
||||
SPEED_HIGH, FanEntity,
|
||||
SUPPORT_SET_SPEED, SUPPORT_OSCILLATE,
|
||||
SPEED_OFF, ATTR_SPEED)
|
||||
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -77,19 +80,32 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
||||
SPEED_MEDIUM, SPEED_HIGH]): cv.ensure_list,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
async_add_entities, discovery_info=None):
|
||||
"""Set up the MQTT fan platform."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
"""Set up MQTT fan through configuration.yaml."""
|
||||
await _async_setup_entity(hass, config, async_add_entities)
|
||||
|
||||
discovery_hash = None
|
||||
if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info:
|
||||
discovery_hash = discovery_info[ATTR_DISCOVERY_HASH]
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up MQTT fan dynamically through MQTT discovery."""
|
||||
async def async_discover(discovery_payload):
|
||||
"""Discover and add a MQTT fan."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
await _async_setup_entity(hass, config, async_add_entities,
|
||||
discovery_payload[ATTR_DISCOVERY_HASH])
|
||||
|
||||
async_dispatcher_connect(
|
||||
hass, MQTT_DISCOVERY_NEW.format(fan.DOMAIN, 'mqtt'),
|
||||
async_discover)
|
||||
|
||||
|
||||
async def _async_setup_entity(hass, config, async_add_entities,
|
||||
discovery_hash=None):
|
||||
"""Set up the MQTT fan."""
|
||||
async_add_entities([MqttFan(
|
||||
config.get(CONF_NAME),
|
||||
{
|
||||
@ -124,21 +140,24 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
config.get(CONF_UNIQUE_ID),
|
||||
config.get(CONF_DEVICE),
|
||||
discovery_hash,
|
||||
)])
|
||||
|
||||
|
||||
class MqttFan(MqttAvailability, MqttDiscoveryUpdate, FanEntity):
|
||||
class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
||||
FanEntity):
|
||||
"""A MQTT fan component."""
|
||||
|
||||
def __init__(self, name, topic, templates, qos, retain, payload,
|
||||
speed_list, optimistic, availability_topic, payload_available,
|
||||
payload_not_available, unique_id: Optional[str],
|
||||
discovery_hash):
|
||||
device_config: Optional[ConfigType], discovery_hash):
|
||||
"""Initialize the MQTT fan."""
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash)
|
||||
MqttEntityDeviceInfo.__init__(self, device_config)
|
||||
self._name = name
|
||||
self._topic = topic
|
||||
self._qos = qos
|
||||
|
@ -224,7 +224,7 @@ class TemplateFan(FanEntity):
|
||||
# pylint: disable=arguments-differ
|
||||
async def async_turn_on(self, speed: str = None) -> None:
|
||||
"""Turn on the fan."""
|
||||
await self._on_script.async_run()
|
||||
await self._on_script.async_run(context=self._context)
|
||||
self._state = STATE_ON
|
||||
|
||||
if speed is not None:
|
||||
@ -233,7 +233,7 @@ class TemplateFan(FanEntity):
|
||||
# pylint: disable=arguments-differ
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn off the fan."""
|
||||
await self._off_script.async_run()
|
||||
await self._off_script.async_run(context=self._context)
|
||||
self._state = STATE_OFF
|
||||
|
||||
async def async_set_speed(self, speed: str) -> None:
|
||||
@ -243,7 +243,8 @@ class TemplateFan(FanEntity):
|
||||
|
||||
if speed in self._speed_list:
|
||||
self._speed = speed
|
||||
await self._set_speed_script.async_run({ATTR_SPEED: speed})
|
||||
await self._set_speed_script.async_run(
|
||||
{ATTR_SPEED: speed}, context=self._context)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Received invalid speed: %s. Expected: %s.',
|
||||
@ -257,7 +258,7 @@ class TemplateFan(FanEntity):
|
||||
if oscillating in _VALID_OSC:
|
||||
self._oscillating = oscillating
|
||||
await self._set_oscillating_script.async_run(
|
||||
{ATTR_OSCILLATING: oscillating})
|
||||
{ATTR_OSCILLATING: oscillating}, context=self._context)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Received invalid oscillating value: %s. Expected: %s.',
|
||||
@ -271,7 +272,7 @@ class TemplateFan(FanEntity):
|
||||
if direction in _VALID_DIRECTIONS:
|
||||
self._direction = direction
|
||||
await self._set_direction_script.async_run(
|
||||
{ATTR_DIRECTION: direction})
|
||||
{ATTR_DIRECTION: direction}, context=self._context)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Received invalid direction: %s. Expected: %s.',
|
||||
|
@ -11,13 +11,15 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA,
|
||||
SUPPORT_SET_SPEED, DOMAIN, )
|
||||
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
|
||||
ATTR_ENTITY_ID, )
|
||||
from homeassistant.components.fan import (
|
||||
DOMAIN, PLATFORM_SCHEMA, SUPPORT_SET_SPEED, FanEntity)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN)
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Xiaomi Miio Device'
|
||||
@ -49,8 +51,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
'zhimi.humidifier.ca1']),
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41']
|
||||
|
||||
ATTR_MODEL = 'model'
|
||||
|
||||
# Air Purifier
|
||||
|
@ -7,11 +7,12 @@ https://home-assistant.io/components/fan.zwave/
|
||||
import logging
|
||||
import math
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.fan import (
|
||||
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||
SUPPORT_SET_SPEED)
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -35,6 +36,22 @@ SPEED_TO_VALUE = {
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Old method of setting up Z-Wave fans."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up Z-Wave Fan from Config Entry."""
|
||||
@callback
|
||||
def async_add_fan(fan):
|
||||
"""Add Z-Wave Fan."""
|
||||
async_add_entities([fan])
|
||||
|
||||
async_dispatcher_connect(hass, 'zwave_new_fan', async_add_fan)
|
||||
|
||||
|
||||
def get_device(values, **kwargs):
|
||||
"""Create Z-Wave entity device."""
|
||||
return ZwaveFan(values)
|
||||
|
@ -24,7 +24,7 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20181018.0']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20181026.0']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',
|
||||
|
@ -1,32 +1,34 @@
|
||||
"""
|
||||
Geo Location component.
|
||||
|
||||
This component covers platforms that deal with external events that contain
|
||||
a geo location related to the installed HA instance.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/geo_location/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DISTANCE = 'distance'
|
||||
ATTR_SOURCE = 'source'
|
||||
|
||||
DOMAIN = 'geo_location'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
GROUP_NAME_ALL_EVENTS = 'All Geo Location Events'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up this component."""
|
||||
"""Set up the Geo Location component."""
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_EVENTS)
|
||||
await component.async_setup(config)
|
||||
@ -43,6 +45,11 @@ class GeoLocationEvent(Entity):
|
||||
return round(self.distance, 1)
|
||||
return None
|
||||
|
||||
@property
|
||||
def source(self) -> str:
|
||||
"""Return source value of this external event."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def distance(self) -> Optional[float]:
|
||||
"""Return distance value of this external event."""
|
||||
@ -66,4 +73,6 @@ class GeoLocationEvent(Entity):
|
||||
data[ATTR_LATITUDE] = round(self.latitude, 5)
|
||||
if self.longitude is not None:
|
||||
data[ATTR_LONGITUDE] = round(self.longitude, 5)
|
||||
if self.source is not None:
|
||||
data[ATTR_SOURCE] = self.source
|
||||
return data
|
||||
|
@ -4,10 +4,10 @@ Demo platform for the geo location component.
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import logging
|
||||
import random
|
||||
from datetime import timedelta
|
||||
from math import pi, cos, sin, radians
|
||||
import logging
|
||||
from math import cos, pi, radians, sin
|
||||
import random
|
||||
from typing import Optional
|
||||
|
||||
from homeassistant.components.geo_location import GeoLocationEvent
|
||||
@ -16,7 +16,7 @@ from homeassistant.helpers.event import track_time_interval
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AVG_KM_PER_DEGREE = 111.0
|
||||
DEFAULT_UNIT_OF_MEASUREMENT = "km"
|
||||
DEFAULT_UNIT_OF_MEASUREMENT = 'km'
|
||||
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1)
|
||||
MAX_RADIUS_IN_KM = 50
|
||||
NUMBER_OF_DEMO_DEVICES = 5
|
||||
@ -26,6 +26,8 @@ EVENT_NAMES = ["Bushfire", "Hazard Reduction", "Grass Fire", "Burn off",
|
||||
"Cyclone", "Waterspout", "Dust Storm", "Blizzard", "Ice Storm",
|
||||
"Earthquake", "Tsunami"]
|
||||
|
||||
SOURCE = 'demo'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Demo geo locations."""
|
||||
@ -100,6 +102,11 @@ class DemoGeoLocationEvent(GeoLocationEvent):
|
||||
self._longitude = longitude
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
|
||||
@property
|
||||
def source(self) -> str:
|
||||
"""Return source value of this external event."""
|
||||
return SOURCE
|
||||
|
||||
@property
|
||||
def name(self) -> Optional[str]:
|
||||
"""Return the name of the event."""
|
||||
|
@ -1,24 +1,23 @@
|
||||
"""
|
||||
Generic GeoJSON events platform.
|
||||
|
||||
Retrieves current events (typically incidents or alerts) in GeoJSON format, and
|
||||
displays information on events filtered by distance to the HA instance's
|
||||
location.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/geo_location/geo_json_events/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.geo_location import (
|
||||
PLATFORM_SCHEMA, GeoLocationEvent)
|
||||
from homeassistant.const import (
|
||||
CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_URL, EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.geo_location import GeoLocationEvent
|
||||
from homeassistant.const import CONF_RADIUS, CONF_URL, CONF_SCAN_INTERVAL, \
|
||||
EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.components.geo_location import PLATFORM_SCHEMA
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect, dispatcher_send)
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
|
||||
REQUIREMENTS = ['geojson_client==0.1']
|
||||
@ -28,14 +27,18 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ATTR_EXTERNAL_ID = 'external_id'
|
||||
|
||||
DEFAULT_RADIUS_IN_KM = 20.0
|
||||
DEFAULT_UNIT_OF_MEASUREMENT = "km"
|
||||
DEFAULT_UNIT_OF_MEASUREMENT = 'km'
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
SIGNAL_DELETE_ENTITY = 'geo_json_events_delete_{}'
|
||||
SIGNAL_UPDATE_ENTITY = 'geo_json_events_update_{}'
|
||||
|
||||
SOURCE = 'geo_json_events'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM):
|
||||
vol.Coerce(float),
|
||||
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float),
|
||||
})
|
||||
|
||||
|
||||
@ -45,7 +48,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
|
||||
radius_in_km = config[CONF_RADIUS]
|
||||
# Initialize the entity manager.
|
||||
GeoJsonFeedManager(hass, add_entities, scan_interval, url, radius_in_km)
|
||||
feed = GeoJsonFeedManager(hass, add_entities, scan_interval, url,
|
||||
radius_in_km)
|
||||
|
||||
def start_feed_manager(event):
|
||||
"""Start feed manager."""
|
||||
feed.startup()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager)
|
||||
|
||||
|
||||
class GeoJsonFeedManager:
|
||||
@ -54,94 +64,117 @@ class GeoJsonFeedManager:
|
||||
def __init__(self, hass, add_entities, scan_interval, url, radius_in_km):
|
||||
"""Initialize the GeoJSON Feed Manager."""
|
||||
from geojson_client.generic_feed import GenericFeed
|
||||
|
||||
self._hass = hass
|
||||
self._feed = GenericFeed((hass.config.latitude, hass.config.longitude),
|
||||
filter_radius=radius_in_km, url=url)
|
||||
self._feed = GenericFeed(
|
||||
(hass.config.latitude, hass.config.longitude),
|
||||
filter_radius=radius_in_km, url=url)
|
||||
self._add_entities = add_entities
|
||||
self._scan_interval = scan_interval
|
||||
self._feed_entries = []
|
||||
self._managed_entities = []
|
||||
hass.bus.listen_once(
|
||||
EVENT_HOMEASSISTANT_START, lambda _: self._update())
|
||||
self.feed_entries = {}
|
||||
self._managed_external_ids = set()
|
||||
|
||||
def startup(self):
|
||||
"""Start up this manager."""
|
||||
self._update()
|
||||
self._init_regular_updates()
|
||||
|
||||
def _init_regular_updates(self):
|
||||
"""Schedule regular updates at the specified interval."""
|
||||
track_time_interval(self._hass, lambda now: self._update(),
|
||||
self._scan_interval)
|
||||
track_time_interval(
|
||||
self._hass, lambda now: self._update(), self._scan_interval)
|
||||
|
||||
def _update(self):
|
||||
"""Update the feed and then update connected entities."""
|
||||
import geojson_client
|
||||
|
||||
status, feed_entries = self._feed.update()
|
||||
if status == geojson_client.UPDATE_OK:
|
||||
_LOGGER.debug("Data retrieved %s", feed_entries)
|
||||
# Keep a copy of all feed entries for future lookups by entities.
|
||||
self._feed_entries = feed_entries.copy()
|
||||
keep_entries = self._update_or_remove_entities(feed_entries)
|
||||
self._generate_new_entities(keep_entries)
|
||||
self.feed_entries = {entry.external_id: entry
|
||||
for entry in feed_entries}
|
||||
# For entity management the external ids from the feed are used.
|
||||
feed_external_ids = set(self.feed_entries)
|
||||
remove_external_ids = self._managed_external_ids.difference(
|
||||
feed_external_ids)
|
||||
self._remove_entities(remove_external_ids)
|
||||
update_external_ids = self._managed_external_ids.intersection(
|
||||
feed_external_ids)
|
||||
self._update_entities(update_external_ids)
|
||||
create_external_ids = feed_external_ids.difference(
|
||||
self._managed_external_ids)
|
||||
self._generate_new_entities(create_external_ids)
|
||||
elif status == geojson_client.UPDATE_OK_NO_DATA:
|
||||
_LOGGER.debug("Update successful, but no data received from %s",
|
||||
self._feed)
|
||||
_LOGGER.debug(
|
||||
"Update successful, but no data received from %s", self._feed)
|
||||
else:
|
||||
_LOGGER.warning("Update not successful, no data received from %s",
|
||||
self._feed)
|
||||
_LOGGER.warning(
|
||||
"Update not successful, no data received from %s", self._feed)
|
||||
# Remove all entities.
|
||||
self._update_or_remove_entities([])
|
||||
self._remove_entities(self._managed_external_ids.copy())
|
||||
|
||||
def _update_or_remove_entities(self, feed_entries):
|
||||
"""Update existing entries and remove obsolete entities."""
|
||||
_LOGGER.debug("Entries for updating: %s", feed_entries)
|
||||
remove_entry = None
|
||||
# Remove obsolete entities for events that have disappeared
|
||||
managed_entities = self._managed_entities.copy()
|
||||
for entity in managed_entities:
|
||||
# Remove entry from previous iteration - if applicable.
|
||||
if remove_entry:
|
||||
feed_entries.remove(remove_entry)
|
||||
remove_entry = None
|
||||
for entry in feed_entries:
|
||||
if entity.external_id == entry.external_id:
|
||||
# Existing entity - update details.
|
||||
_LOGGER.debug("Existing entity found %s", entity)
|
||||
remove_entry = entry
|
||||
entity.schedule_update_ha_state(True)
|
||||
break
|
||||
else:
|
||||
# Remove obsolete entity.
|
||||
_LOGGER.debug("Entity not current anymore %s", entity)
|
||||
self._managed_entities.remove(entity)
|
||||
self._hass.add_job(entity.async_remove())
|
||||
# Remove entry from very last iteration - if applicable.
|
||||
if remove_entry:
|
||||
feed_entries.remove(remove_entry)
|
||||
# Return the remaining entries that new entities must be created for.
|
||||
return feed_entries
|
||||
|
||||
def _generate_new_entities(self, entries):
|
||||
def _generate_new_entities(self, external_ids):
|
||||
"""Generate new entities for events."""
|
||||
new_entities = []
|
||||
for entry in entries:
|
||||
new_entity = GeoJsonLocationEvent(self, entry)
|
||||
_LOGGER.debug("New entity added %s", new_entity)
|
||||
for external_id in external_ids:
|
||||
new_entity = GeoJsonLocationEvent(self, external_id)
|
||||
_LOGGER.debug("New entity added %s", external_id)
|
||||
new_entities.append(new_entity)
|
||||
# Add new entities to HA and keep track of them in this manager.
|
||||
self._managed_external_ids.add(external_id)
|
||||
# Add new entities to HA.
|
||||
self._add_entities(new_entities, True)
|
||||
self._managed_entities.extend(new_entities)
|
||||
|
||||
def get_feed_entry(self, external_id):
|
||||
"""Return a feed entry identified by external id."""
|
||||
return next((entry for entry in self._feed_entries
|
||||
if entry.external_id == external_id), None)
|
||||
def _update_entities(self, external_ids):
|
||||
"""Update entities."""
|
||||
for external_id in external_ids:
|
||||
_LOGGER.debug("Existing entity found %s", external_id)
|
||||
dispatcher_send(
|
||||
self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
|
||||
|
||||
def _remove_entities(self, external_ids):
|
||||
"""Remove entities."""
|
||||
for external_id in external_ids:
|
||||
_LOGGER.debug("Entity not current anymore %s", external_id)
|
||||
self._managed_external_ids.remove(external_id)
|
||||
dispatcher_send(
|
||||
self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
|
||||
|
||||
|
||||
class GeoJsonLocationEvent(GeoLocationEvent):
|
||||
"""This represents an external event with GeoJSON data."""
|
||||
|
||||
def __init__(self, feed_manager, feed_entry):
|
||||
def __init__(self, feed_manager, external_id):
|
||||
"""Initialize entity with data from feed entry."""
|
||||
self._feed_manager = feed_manager
|
||||
self._update_from_feed(feed_entry)
|
||||
self._external_id = external_id
|
||||
self._name = None
|
||||
self._distance = None
|
||||
self._latitude = None
|
||||
self._longitude = None
|
||||
self._remove_signal_delete = None
|
||||
self._remove_signal_update = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity is added to hass."""
|
||||
self._remove_signal_delete = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_DELETE_ENTITY.format(self._external_id),
|
||||
self._delete_callback)
|
||||
self._remove_signal_update = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_ENTITY.format(self._external_id),
|
||||
self._update_callback)
|
||||
|
||||
@callback
|
||||
def _delete_callback(self):
|
||||
"""Remove this entity."""
|
||||
self._remove_signal_delete()
|
||||
self._remove_signal_update()
|
||||
self.hass.async_create_task(self.async_remove())
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@ -150,7 +183,8 @@ class GeoJsonLocationEvent(GeoLocationEvent):
|
||||
|
||||
async def async_update(self):
|
||||
"""Update this entity from the data held in the feed manager."""
|
||||
feed_entry = self._feed_manager.get_feed_entry(self.external_id)
|
||||
_LOGGER.debug("Updating %s", self._external_id)
|
||||
feed_entry = self._feed_manager.feed_entries.get(self._external_id)
|
||||
if feed_entry:
|
||||
self._update_from_feed(feed_entry)
|
||||
|
||||
@ -160,7 +194,11 @@ class GeoJsonLocationEvent(GeoLocationEvent):
|
||||
self._distance = feed_entry.distance_to_home
|
||||
self._latitude = feed_entry.coordinates[0]
|
||||
self._longitude = feed_entry.coordinates[1]
|
||||
self.external_id = feed_entry.external_id
|
||||
|
||||
@property
|
||||
def source(self) -> str:
|
||||
"""Return source value of this external event."""
|
||||
return SOURCE
|
||||
|
||||
@property
|
||||
def name(self) -> Optional[str]:
|
||||
@ -191,6 +229,6 @@ class GeoJsonLocationEvent(GeoLocationEvent):
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
attributes = {}
|
||||
if self.external_id:
|
||||
attributes[ATTR_EXTERNAL_ID] = self.external_id
|
||||
if self._external_id:
|
||||
attributes[ATTR_EXTERNAL_ID] = self._external_id
|
||||
return attributes
|
||||
|
@ -0,0 +1,288 @@
|
||||
"""
|
||||
NSW Rural Fire Service Feed platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/geo_location/nsw_rural_fire_service_feed/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.geo_location import (
|
||||
PLATFORM_SCHEMA, GeoLocationEvent)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_RADIUS, CONF_SCAN_INTERVAL,
|
||||
EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect, dispatcher_send)
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
|
||||
REQUIREMENTS = ['geojson_client==0.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_CATEGORY = 'category'
|
||||
ATTR_COUNCIL_AREA = 'council_area'
|
||||
ATTR_EXTERNAL_ID = 'external_id'
|
||||
ATTR_FIRE = 'fire'
|
||||
ATTR_PUBLICATION_DATE = 'publication_date'
|
||||
ATTR_RESPONSIBLE_AGENCY = 'responsible_agency'
|
||||
ATTR_SIZE = 'size'
|
||||
ATTR_STATUS = 'status'
|
||||
ATTR_TYPE = 'type'
|
||||
|
||||
CONF_CATEGORIES = 'categories'
|
||||
|
||||
DEFAULT_RADIUS_IN_KM = 20.0
|
||||
DEFAULT_UNIT_OF_MEASUREMENT = 'km'
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
SIGNAL_DELETE_ENTITY = 'nsw_rural_fire_service_feed_delete_{}'
|
||||
SIGNAL_UPDATE_ENTITY = 'nsw_rural_fire_service_feed_update_{}'
|
||||
|
||||
SOURCE = 'nsw_rural_fire_service_feed'
|
||||
|
||||
VALID_CATEGORIES = [
|
||||
'Advice',
|
||||
'Emergency Warning',
|
||||
'Not Applicable',
|
||||
'Watch and Act',
|
||||
]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_CATEGORIES, default=[]):
|
||||
vol.All(cv.ensure_list, [vol.In(VALID_CATEGORIES)]),
|
||||
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the GeoJSON Events platform."""
|
||||
scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
|
||||
radius_in_km = config[CONF_RADIUS]
|
||||
categories = config.get(CONF_CATEGORIES)
|
||||
# Initialize the entity manager.
|
||||
feed = NswRuralFireServiceFeedManager(
|
||||
hass, add_entities, scan_interval, radius_in_km, categories)
|
||||
|
||||
def start_feed_manager(event):
|
||||
"""Start feed manager."""
|
||||
feed.startup()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager)
|
||||
|
||||
|
||||
class NswRuralFireServiceFeedManager:
|
||||
"""Feed Manager for NSW Rural Fire Service GeoJSON feed."""
|
||||
|
||||
def __init__(self, hass, add_entities, scan_interval, radius_in_km,
|
||||
categories):
|
||||
"""Initialize the GeoJSON Feed Manager."""
|
||||
from geojson_client.nsw_rural_fire_service_feed \
|
||||
import NswRuralFireServiceFeed
|
||||
|
||||
self._hass = hass
|
||||
self._feed = NswRuralFireServiceFeed(
|
||||
(hass.config.latitude, hass.config.longitude),
|
||||
filter_radius=radius_in_km, filter_categories=categories)
|
||||
self._add_entities = add_entities
|
||||
self._scan_interval = scan_interval
|
||||
self.feed_entries = {}
|
||||
self._managed_external_ids = set()
|
||||
|
||||
def startup(self):
|
||||
"""Start up this manager."""
|
||||
self._update()
|
||||
self._init_regular_updates()
|
||||
|
||||
def _init_regular_updates(self):
|
||||
"""Schedule regular updates at the specified interval."""
|
||||
track_time_interval(
|
||||
self._hass, lambda now: self._update(), self._scan_interval)
|
||||
|
||||
def _update(self):
|
||||
"""Update the feed and then update connected entities."""
|
||||
import geojson_client
|
||||
|
||||
status, feed_entries = self._feed.update()
|
||||
if status == geojson_client.UPDATE_OK:
|
||||
_LOGGER.debug("Data retrieved %s", feed_entries)
|
||||
# Keep a copy of all feed entries for future lookups by entities.
|
||||
self.feed_entries = {entry.external_id: entry
|
||||
for entry in feed_entries}
|
||||
# For entity management the external ids from the feed are used.
|
||||
feed_external_ids = set(self.feed_entries)
|
||||
remove_external_ids = self._managed_external_ids.difference(
|
||||
feed_external_ids)
|
||||
self._remove_entities(remove_external_ids)
|
||||
update_external_ids = self._managed_external_ids.intersection(
|
||||
feed_external_ids)
|
||||
self._update_entities(update_external_ids)
|
||||
create_external_ids = feed_external_ids.difference(
|
||||
self._managed_external_ids)
|
||||
self._generate_new_entities(create_external_ids)
|
||||
elif status == geojson_client.UPDATE_OK_NO_DATA:
|
||||
_LOGGER.debug(
|
||||
"Update successful, but no data received from %s", self._feed)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Update not successful, no data received from %s", self._feed)
|
||||
# Remove all entities.
|
||||
self._remove_entities(self._managed_external_ids.copy())
|
||||
|
||||
def _generate_new_entities(self, external_ids):
|
||||
"""Generate new entities for events."""
|
||||
new_entities = []
|
||||
for external_id in external_ids:
|
||||
new_entity = NswRuralFireServiceLocationEvent(self, external_id)
|
||||
_LOGGER.debug("New entity added %s", external_id)
|
||||
new_entities.append(new_entity)
|
||||
self._managed_external_ids.add(external_id)
|
||||
# Add new entities to HA.
|
||||
self._add_entities(new_entities, True)
|
||||
|
||||
def _update_entities(self, external_ids):
|
||||
"""Update entities."""
|
||||
for external_id in external_ids:
|
||||
_LOGGER.debug("Existing entity found %s", external_id)
|
||||
dispatcher_send(
|
||||
self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
|
||||
|
||||
def _remove_entities(self, external_ids):
|
||||
"""Remove entities."""
|
||||
for external_id in external_ids:
|
||||
_LOGGER.debug("Entity not current anymore %s", external_id)
|
||||
self._managed_external_ids.remove(external_id)
|
||||
dispatcher_send(
|
||||
self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
|
||||
|
||||
|
||||
class NswRuralFireServiceLocationEvent(GeoLocationEvent):
|
||||
"""This represents an external event with GeoJSON data."""
|
||||
|
||||
def __init__(self, feed_manager, external_id):
|
||||
"""Initialize entity with data from feed entry."""
|
||||
self._feed_manager = feed_manager
|
||||
self._external_id = external_id
|
||||
self._name = None
|
||||
self._distance = None
|
||||
self._latitude = None
|
||||
self._longitude = None
|
||||
self._attribution = None
|
||||
self._category = None
|
||||
self._publication_date = None
|
||||
self._location = None
|
||||
self._council_area = None
|
||||
self._status = None
|
||||
self._type = None
|
||||
self._fire = None
|
||||
self._size = None
|
||||
self._responsible_agency = None
|
||||
self._remove_signal_delete = None
|
||||
self._remove_signal_update = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity is added to hass."""
|
||||
self._remove_signal_delete = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_DELETE_ENTITY.format(self._external_id),
|
||||
self._delete_callback)
|
||||
self._remove_signal_update = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_ENTITY.format(self._external_id),
|
||||
self._update_callback)
|
||||
|
||||
@callback
|
||||
def _delete_callback(self):
|
||||
"""Remove this entity."""
|
||||
self._remove_signal_delete()
|
||||
self._remove_signal_update()
|
||||
self.hass.async_create_task(self.async_remove())
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for GeoJSON location events."""
|
||||
return False
|
||||
|
||||
async def async_update(self):
|
||||
"""Update this entity from the data held in the feed manager."""
|
||||
_LOGGER.debug("Updating %s", self._external_id)
|
||||
feed_entry = self._feed_manager.feed_entries.get(self._external_id)
|
||||
if feed_entry:
|
||||
self._update_from_feed(feed_entry)
|
||||
|
||||
def _update_from_feed(self, feed_entry):
|
||||
"""Update the internal state from the provided feed entry."""
|
||||
self._name = feed_entry.title
|
||||
self._distance = feed_entry.distance_to_home
|
||||
self._latitude = feed_entry.coordinates[0]
|
||||
self._longitude = feed_entry.coordinates[1]
|
||||
self._attribution = feed_entry.attribution
|
||||
self._category = feed_entry.category
|
||||
self._publication_date = feed_entry.publication_date
|
||||
self._location = feed_entry.location
|
||||
self._council_area = feed_entry.council_area
|
||||
self._status = feed_entry.status
|
||||
self._type = feed_entry.type
|
||||
self._fire = feed_entry.fire
|
||||
self._size = feed_entry.size
|
||||
self._responsible_agency = feed_entry.responsible_agency
|
||||
|
||||
@property
|
||||
def source(self) -> str:
|
||||
"""Return source value of this external event."""
|
||||
return SOURCE
|
||||
|
||||
@property
|
||||
def name(self) -> Optional[str]:
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def distance(self) -> Optional[float]:
|
||||
"""Return distance value of this external event."""
|
||||
return self._distance
|
||||
|
||||
@property
|
||||
def latitude(self) -> Optional[float]:
|
||||
"""Return latitude value of this external event."""
|
||||
return self._latitude
|
||||
|
||||
@property
|
||||
def longitude(self) -> Optional[float]:
|
||||
"""Return longitude value of this external event."""
|
||||
return self._longitude
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return DEFAULT_UNIT_OF_MEASUREMENT
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
attributes = {}
|
||||
for key, value in (
|
||||
(ATTR_EXTERNAL_ID, self._external_id),
|
||||
(ATTR_CATEGORY, self._category),
|
||||
(ATTR_LOCATION, self._location),
|
||||
(ATTR_ATTRIBUTION, self._attribution),
|
||||
(ATTR_PUBLICATION_DATE, self._publication_date),
|
||||
(ATTR_COUNCIL_AREA, self._council_area),
|
||||
(ATTR_STATUS, self._status),
|
||||
(ATTR_TYPE, self._type),
|
||||
(ATTR_FIRE, self._fire),
|
||||
(ATTR_SIZE, self._size),
|
||||
(ATTR_RESPONSIBLE_AGENCY, self._responsible_agency),
|
||||
):
|
||||
if value or isinstance(value, bool):
|
||||
attributes[key] = value
|
||||
return attributes
|
@ -12,7 +12,7 @@ import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
REQUIREMENTS = ['pysher==0.2.0']
|
||||
REQUIREMENTS = ['pysher==1.0.4']
|
||||
|
||||
DOMAIN = 'goalfeed'
|
||||
|
||||
|
@ -144,8 +144,7 @@ class GraphiteFeeder(threading.Thread):
|
||||
try:
|
||||
self._report_attributes(
|
||||
event.data['entity_id'], event.data['new_state'])
|
||||
# pylint: disable=broad-except
|
||||
except Exception:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# Catch this so we can avoid the thread dying and
|
||||
# make it visible.
|
||||
_LOGGER.exception("Failed to process STATE_CHANGED event")
|
||||
|
@ -30,6 +30,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
CONF_ENTITIES = 'entities'
|
||||
CONF_VIEW = 'view'
|
||||
CONF_CONTROL = 'control'
|
||||
CONF_ALL = 'all'
|
||||
|
||||
ATTR_ADD_ENTITIES = 'add_entities'
|
||||
ATTR_AUTO = 'auto'
|
||||
@ -39,6 +40,7 @@ ATTR_OBJECT_ID = 'object_id'
|
||||
ATTR_ORDER = 'order'
|
||||
ATTR_VIEW = 'view'
|
||||
ATTR_VISIBLE = 'visible'
|
||||
ATTR_ALL = 'all'
|
||||
|
||||
SERVICE_SET_VISIBILITY = 'set_visibility'
|
||||
SERVICE_SET = 'set'
|
||||
@ -60,6 +62,7 @@ SET_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ICON): cv.string,
|
||||
vol.Optional(ATTR_CONTROL): CONTROL_TYPES,
|
||||
vol.Optional(ATTR_VISIBLE): cv.boolean,
|
||||
vol.Optional(ATTR_ALL): cv.boolean,
|
||||
vol.Exclusive(ATTR_ENTITIES, 'entities'): cv.entity_ids,
|
||||
vol.Exclusive(ATTR_ADD_ENTITIES, 'entities'): cv.entity_ids,
|
||||
})
|
||||
@ -85,6 +88,7 @@ GROUP_SCHEMA = vol.Schema({
|
||||
CONF_NAME: cv.string,
|
||||
CONF_ICON: cv.icon,
|
||||
CONF_CONTROL: CONTROL_TYPES,
|
||||
CONF_ALL: cv.boolean,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
@ -223,6 +227,7 @@ async def async_setup(hass, config):
|
||||
object_id=object_id,
|
||||
entity_ids=entity_ids,
|
||||
user_defined=False,
|
||||
mode=service.data.get(ATTR_ALL),
|
||||
**extra_arg
|
||||
)
|
||||
return
|
||||
@ -265,6 +270,10 @@ async def async_setup(hass, config):
|
||||
group.view = service.data[ATTR_VIEW]
|
||||
need_update = True
|
||||
|
||||
if ATTR_ALL in service.data:
|
||||
group.mode = all if service.data[ATTR_ALL] else any
|
||||
need_update = True
|
||||
|
||||
if need_update:
|
||||
await group.async_update_ha_state()
|
||||
|
||||
@ -310,19 +319,21 @@ async def _async_process_config(hass, config, component):
|
||||
icon = conf.get(CONF_ICON)
|
||||
view = conf.get(CONF_VIEW)
|
||||
control = conf.get(CONF_CONTROL)
|
||||
mode = conf.get(CONF_ALL)
|
||||
|
||||
# Don't create tasks and await them all. The order is important as
|
||||
# groups get a number based on creation order.
|
||||
await Group.async_create_group(
|
||||
hass, name, entity_ids, icon=icon, view=view,
|
||||
control=control, object_id=object_id)
|
||||
control=control, object_id=object_id, mode=mode)
|
||||
|
||||
|
||||
class Group(Entity):
|
||||
"""Track a group of entity ids."""
|
||||
|
||||
def __init__(self, hass, name, order=None, visible=True, icon=None,
|
||||
view=False, control=None, user_defined=True, entity_ids=None):
|
||||
view=False, control=None, user_defined=True, entity_ids=None,
|
||||
mode=None):
|
||||
"""Initialize a group.
|
||||
|
||||
This Object has factory function for creation.
|
||||
@ -341,6 +352,9 @@ class Group(Entity):
|
||||
self.visible = visible
|
||||
self.control = control
|
||||
self.user_defined = user_defined
|
||||
self.mode = any
|
||||
if mode:
|
||||
self.mode = all
|
||||
self._order = order
|
||||
self._assumed_state = False
|
||||
self._async_unsub_state_changed = None
|
||||
@ -348,18 +362,19 @@ class Group(Entity):
|
||||
@staticmethod
|
||||
def create_group(hass, name, entity_ids=None, user_defined=True,
|
||||
visible=True, icon=None, view=False, control=None,
|
||||
object_id=None):
|
||||
object_id=None, mode=None):
|
||||
"""Initialize a group."""
|
||||
return run_coroutine_threadsafe(
|
||||
Group.async_create_group(
|
||||
hass, name, entity_ids, user_defined, visible, icon, view,
|
||||
control, object_id),
|
||||
control, object_id, mode),
|
||||
hass.loop).result()
|
||||
|
||||
@staticmethod
|
||||
async def async_create_group(hass, name, entity_ids=None,
|
||||
user_defined=True, visible=True, icon=None,
|
||||
view=False, control=None, object_id=None):
|
||||
view=False, control=None, object_id=None,
|
||||
mode=None):
|
||||
"""Initialize a group.
|
||||
|
||||
This method must be run in the event loop.
|
||||
@ -368,7 +383,7 @@ class Group(Entity):
|
||||
hass, name,
|
||||
order=len(hass.states.async_entity_ids(DOMAIN)),
|
||||
visible=visible, icon=icon, view=view, control=control,
|
||||
user_defined=user_defined, entity_ids=entity_ids
|
||||
user_defined=user_defined, entity_ids=entity_ids, mode=mode
|
||||
)
|
||||
|
||||
group.entity_id = async_generate_entity_id(
|
||||
@ -557,13 +572,16 @@ class Group(Entity):
|
||||
if gr_on is None:
|
||||
return
|
||||
|
||||
# pylint: disable=too-many-boolean-expressions
|
||||
if tr_state is None or ((gr_state == gr_on and
|
||||
tr_state.state == gr_off) or
|
||||
(gr_state == gr_off and
|
||||
tr_state.state == gr_on) or
|
||||
tr_state.state not in (gr_on, gr_off)):
|
||||
if states is None:
|
||||
states = self._tracking_states
|
||||
|
||||
if any(state.state == gr_on for state in states):
|
||||
if self.mode(state.state == gr_on for state in states):
|
||||
self._state = gr_on
|
||||
else:
|
||||
self._state = gr_off
|
||||
@ -576,7 +594,7 @@ class Group(Entity):
|
||||
if states is None:
|
||||
states = self._tracking_states
|
||||
|
||||
self._assumed_state = any(
|
||||
self._assumed_state = self.mode(
|
||||
state.attributes.get(ATTR_ASSUMED_STATE) for state
|
||||
in states)
|
||||
|
||||
|
@ -40,6 +40,9 @@ set:
|
||||
add_entities:
|
||||
description: List of members they will change on group listening.
|
||||
example: domain.entity_id1, domain.entity_id2
|
||||
all:
|
||||
description: Enable this option if the group should only turn on when all entities are on.
|
||||
example: True
|
||||
|
||||
remove:
|
||||
description: Remove a user group.
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user