diff --git a/.coveragerc b/.coveragerc
index d5296455981..0049349cfff 100644
--- a/.coveragerc
+++ b/.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
diff --git a/CODEOWNERS b/CODEOWNERS
index 91d5fd67670..c49af4864a9 100644
--- a/CODEOWNERS
+++ b/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
diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py
index c6f978640f6..e584d5b70e5 100644
--- a/homeassistant/auth/__init__.py
+++ b/homeassistant/auth/__init__.py
@@ -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)
diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py
index 54c34d8ec2c..8c328bfe13e 100644
--- a/homeassistant/auth/auth_store.py
+++ b/homeassistant/auth/auth_store.py
@@ -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
diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py
index bd00ca72b83..fc35f1398db 100644
--- a/homeassistant/auth/models.py
+++ b/homeassistant/auth/models.py
@@ -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:
diff --git a/homeassistant/auth/permissions.py b/homeassistant/auth/permissions.py
new file mode 100644
index 00000000000..82de61da7f9
--- /dev/null
+++ b/homeassistant/auth/permissions.py
@@ -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
diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py
index e2701ee37f1..bdb89dd60fa 100644
--- a/homeassistant/components/__init__.py
+++ b/homeassistant/components/__init__.py
@@ -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."""
diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py
index 64bedb4ac7c..99bc026a532 100644
--- a/homeassistant/components/abode.py
+++ b/homeassistant/components/abode.py
@@ -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__)
diff --git a/homeassistant/components/alarm_control_panel/elkm1.py b/homeassistant/components/alarm_control_panel/elkm1.py
new file mode 100644
index 00000000000..7b8d2e4ac42
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/elkm1.py
@@ -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)
diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml
index 391de2033c7..7918631464f 100644
--- a/homeassistant/components/alarm_control_panel/services.yaml
+++ b/homeassistant/components/alarm_control_panel/services.yaml
@@ -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.
diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py
index 34c68f26c2a..cdcdf07c982 100644
--- a/homeassistant/components/alarm_control_panel/simplisafe.py
+++ b/homeassistant/components/alarm_control_panel/simplisafe.py
@@ -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
diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py
index f594a798dce..2989bb1be37 100644
--- a/homeassistant/components/alarm_control_panel/totalconnect.py
+++ b/homeassistant/components/alarm_control_panel/totalconnect.py
@@ -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__)
diff --git a/homeassistant/components/apcupsd.py b/homeassistant/components/apcupsd.py
index 8808cee79a3..79b88378169 100644
--- a/homeassistant/components/apcupsd.py
+++ b/homeassistant/components/apcupsd.py
@@ -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
diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py
index 015e1e0d1fc..f7d9f012f65 100644
--- a/homeassistant/components/arlo.py
+++ b/homeassistant/components/arlo.py
@@ -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)
diff --git a/homeassistant/components/asterisk_mbox.py b/homeassistant/components/asterisk_mbox.py
index 0907e48b256..406774d5fad 100644
--- a/homeassistant/components/asterisk_mbox.py
+++ b/homeassistant/components/asterisk_mbox.py
@@ -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)
diff --git a/homeassistant/components/august.py b/homeassistant/components/august.py
index 5f268a95f5d..850d972c373 100644
--- a/homeassistant/components/august.py
+++ b/homeassistant/components/august.py
@@ -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
diff --git a/homeassistant/components/auth/.translations/ro.json b/homeassistant/components/auth/.translations/ro.json
new file mode 100644
index 00000000000..19f9ec10c73
--- /dev/null
+++ b/homeassistant/components/auth/.translations/ro.json
@@ -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"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py
new file mode 100644
index 00000000000..b2c9a9c093a
--- /dev/null
+++ b/homeassistant/components/automation/geo_location.py
@@ -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)
diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py
index 2c9c331cdc5..345b0fe3249 100644
--- a/homeassistant/components/automation/webhook.py
+++ b/homeassistant/components/automation/webhook.py
@@ -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',
diff --git a/homeassistant/components/axis.py b/homeassistant/components/axis.py
index 71894364f91..63fce8a74ee 100644
--- a/homeassistant/components/axis.py
+++ b/homeassistant/components/axis.py
@@ -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,
})
diff --git a/homeassistant/components/binary_sensor/august.py b/homeassistant/components/binary_sensor/august.py
index 7f5da390906..55b31a6da5f 100644
--- a/homeassistant/components/binary_sensor/august.py
+++ b/homeassistant/components/binary_sensor/august.py
@@ -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)
diff --git a/homeassistant/components/binary_sensor/bloomsky.py b/homeassistant/components/binary_sensor/bloomsky.py
index ecffb3accf3..971941f4dd6 100644
--- a/homeassistant/components/binary_sensor/bloomsky.py
+++ b/homeassistant/components/binary_sensor/bloomsky.py
@@ -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):
diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py
index 3fe8136c93b..f8855b2e28b 100644
--- a/homeassistant/components/binary_sensor/bmw_connected_drive.py
+++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py
@@ -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):
diff --git a/homeassistant/components/binary_sensor/knx.py b/homeassistant/components/binary_sensor/knx.py
index d0707b0f067..a89d5d1c945 100644
--- a/homeassistant/components/binary_sensor/knx.py
+++ b/homeassistant/components/binary_sensor/knx.py
@@ -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."""
diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py
index baaf6a9a567..beaeb9ce21b 100644
--- a/homeassistant/components/binary_sensor/mqtt.py
+++ b/homeassistant/components/binary_sensor/mqtt.py
@@ -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(
diff --git a/homeassistant/components/binary_sensor/octoprint.py b/homeassistant/components/binary_sensor/octoprint.py
index 3dd1ee2be8c..285495c03a0 100644
--- a/homeassistant/components/binary_sensor/octoprint.py
+++ b/homeassistant/components/binary_sensor/octoprint.py
@@ -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)
diff --git a/homeassistant/components/binary_sensor/opentherm_gw.py b/homeassistant/components/binary_sensor/opentherm_gw.py
new file mode 100644
index 00000000000..8c5ff8c44d1
--- /dev/null
+++ b/homeassistant/components/binary_sensor/opentherm_gw.py
@@ -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
diff --git a/homeassistant/components/binary_sensor/openuv.py b/homeassistant/components/binary_sensor/openuv.py
index bd6e4d1d5dc..3e9bb0b0bc3 100644
--- a/homeassistant/components/binary_sensor/openuv.py
+++ b/homeassistant/components/binary_sensor/openuv.py
@@ -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."""
diff --git a/homeassistant/components/binary_sensor/ping.py b/homeassistant/components/binary_sensor/ping.py
index f12957e6129..4c597dd63e1 100644
--- a/homeassistant/components/binary_sensor/ping.py
+++ b/homeassistant/components/binary_sensor/ping.py
@@ -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)
diff --git a/homeassistant/components/binary_sensor/rflink.py b/homeassistant/components/binary_sensor/rflink.py
new file mode 100644
index 00000000000..73b912d62da
--- /dev/null
+++ b/homeassistant/components/binary_sensor/rflink.py
@@ -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
diff --git a/homeassistant/components/binary_sensor/ring.py b/homeassistant/components/binary_sensor/ring.py
index 3945eb5c926..5a65917f40b 100644
--- a/homeassistant/components/binary_sensor/ring.py
+++ b/homeassistant/components/binary_sensor/ring.py
@@ -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."""
diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py
index 0b168e45b4d..d332c668703 100644
--- a/homeassistant/components/binary_sensor/trend.py
+++ b/homeassistant/components/binary_sensor/trend.py
@@ -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__)
diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py
index 82b5e66629a..fc8207f83b7 100644
--- a/homeassistant/components/binary_sensor/workday.py
+++ b/homeassistant/components/binary_sensor/workday.py
@@ -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']
diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py
index 730b662b90b..e082c886f03 100644
--- a/homeassistant/components/binary_sensor/xiaomi_aqara.py
+++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py
@@ -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."""
diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py
index aa07a673c97..fa24ed89980 100644
--- a/homeassistant/components/binary_sensor/zha.py
+++ b/homeassistant/components/binary_sensor/zha.py
@@ -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)
diff --git a/homeassistant/components/binary_sensor/zwave.py b/homeassistant/components/binary_sensor/zwave.py
index 3bb3a3c79c5..ca07986976d 100644
--- a/homeassistant/components/binary_sensor/zwave.py
+++ b/homeassistant/components/binary_sensor/zwave.py
@@ -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)
diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py
index abdbc1a2e92..66cfe3990a3 100644
--- a/homeassistant/components/blink/__init__.py
+++ b/homeassistant/components/blink/__init__.py
@@ -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__)
diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py
index dce5961d70d..40f2b91045a 100644
--- a/homeassistant/components/bmw_connected_drive/__init__.py
+++ b/homeassistant/components/bmw_connected_drive/__init__.py
@@ -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)
diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py
index 041b98dc24b..abb4fd28dd4 100644
--- a/homeassistant/components/calendar/google.py
+++ b/homeassistant/components/calendar/google.py
@@ -68,6 +68,7 @@ class GoogleCalendarData:
self.event = None
def _prepare_query(self):
+ # pylint: disable=import-error
from httplib2 import ServerNotFoundError
try:
diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py
index b5eaed4e6c9..a0a3457667f 100644
--- a/homeassistant/components/calendar/todoist.py
+++ b/homeassistant/components/calendar/todoist.py
@@ -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
diff --git a/homeassistant/components/camera/bloomsky.py b/homeassistant/components/camera/bloomsky.py
index 01e20e3ccd3..1c9266ca3a7 100644
--- a/homeassistant/components/camera/bloomsky.py
+++ b/homeassistant/components/camera/bloomsky.py
@@ -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."""
diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py
index c458188695a..dfbcc4d70bc 100644
--- a/homeassistant/components/camera/ffmpeg.py
+++ b/homeassistant/components/camera/ffmpeg.py
@@ -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)])
diff --git a/homeassistant/components/camera/neato.py b/homeassistant/components/camera/neato.py
index b080dbbae10..4df423344bb 100644
--- a/homeassistant/components/camera/neato.py
+++ b/homeassistant/components/camera/neato.py
@@ -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
diff --git a/homeassistant/components/camera/ring.py b/homeassistant/components/camera/ring.py
index ae886bd0669..eafb3066e48 100644
--- a/homeassistant/components/camera/ring.py
+++ b/homeassistant/components/camera/ring.py
@@ -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."""
diff --git a/homeassistant/components/camera/xiaomi.py b/homeassistant/components/camera/xiaomi.py
index da36299a209..3d6b51cf229 100644
--- a/homeassistant/components/camera/xiaomi.py
+++ b/homeassistant/components/camera/xiaomi.py
@@ -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,
diff --git a/homeassistant/components/camera/yi.py b/homeassistant/components/camera/yi.py
index eb26c1cc887..f3800ee0648 100644
--- a/homeassistant/components/camera/yi.py
+++ b/homeassistant/components/camera/yi.py
@@ -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,
diff --git a/homeassistant/components/cast/.translations/pt.json b/homeassistant/components/cast/.translations/pt.json
index a6d28538396..85d1b14484d 100644
--- a/homeassistant/components/cast/.translations/pt.json
+++ b/homeassistant/components/cast/.translations/pt.json
@@ -7,9 +7,9 @@
"step": {
"confirm": {
"description": "Deseja configurar o Google Cast?",
- "title": ""
+ "title": "Google Cast"
}
},
- "title": ""
+ "title": "Google Cast"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/.translations/ro.json b/homeassistant/components/cast/.translations/ro.json
new file mode 100644
index 00000000000..8a1d19c0ecf
--- /dev/null
+++ b/homeassistant/components/cast/.translations/ro.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py
index 98483c454bc..a165521f0bd 100644
--- a/homeassistant/components/climate/__init__.py
+++ b/homeassistant/components/climate/__init__.py
@@ -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
diff --git a/homeassistant/components/climate/daikin.py b/homeassistant/components/climate/daikin.py
index 6743bf034dc..66e380ad68d 100644
--- a/homeassistant/components/climate/daikin.py
+++ b/homeassistant/components/climate/daikin.py
@@ -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)
diff --git a/homeassistant/components/climate/dyson.py b/homeassistant/components/climate/dyson.py
new file mode 100644
index 00000000000..0b09ec7f0b4
--- /dev/null
+++ b/homeassistant/components/climate/dyson.py
@@ -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
diff --git a/homeassistant/components/climate/elkm1.py b/homeassistant/components/climate/elkm1.py
new file mode 100644
index 00000000000..6bd33b382dc
--- /dev/null
+++ b/homeassistant/components/climate/elkm1.py
@@ -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
diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py
index 904d8222e88..bb0a9d4b810 100644
--- a/homeassistant/components/climate/eq3btsmart.py
+++ b/homeassistant/components/climate/eq3btsmart.py
@@ -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:
diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/climate/knx.py
index 4eada356653..b52bd4b418d 100644
--- a/homeassistant/components/climate/knx.py
+++ b/homeassistant/components/climate/knx.py
@@ -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."""
diff --git a/homeassistant/components/climate/mill.py b/homeassistant/components/climate/mill.py
new file mode 100644
index 00000000000..763e239689b
--- /dev/null
+++ b/homeassistant/components/climate/mill.py
@@ -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)
diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py
index 79c49db7955..b107710fea5 100644
--- a/homeassistant/components/climate/mqtt.py
+++ b/homeassistant/components/climate/mqtt.py
@@ -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
diff --git a/homeassistant/components/climate/opentherm_gw.py b/homeassistant/components/climate/opentherm_gw.py
index 00049d26b7f..6dc52e6acc7 100644
--- a/homeassistant/components/climate/opentherm_gw.py
+++ b/homeassistant/components/climate/opentherm_gw.py
@@ -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()
diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml
index fbb21962c6e..e2a42770cb2 100644
--- a/homeassistant/components/climate/services.yaml
+++ b/homeassistant/components/climate/services.yaml
@@ -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:
diff --git a/homeassistant/components/climate/toon.py b/homeassistant/components/climate/toon.py
index e759e922ee1..5972ff52a8b 100644
--- a/homeassistant/components/climate/toon.py
+++ b/homeassistant/components/climate/toon.py
@@ -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',
diff --git a/homeassistant/components/climate/tuya.py b/homeassistant/components/climate/tuya.py
index 2da46fee15d..4548867a45e 100644
--- a/homeassistant/components/climate/tuya.py
+++ b/homeassistant/components/climate/tuya.py
@@ -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()}
diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py
index cb6204d3ba3..7e5230ba3c7 100644
--- a/homeassistant/components/climate/wink.py
+++ b/homeassistant/components/climate/wink.py
@@ -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()
diff --git a/homeassistant/components/climate/zhong_hong.py b/homeassistant/components/climate/zhong_hong.py
index 46d590a9412..b564e9d1fa4 100644
--- a/homeassistant/components/climate/zhong_hong.py
+++ b/homeassistant/components/climate/zhong_hong.py
@@ -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,
})
diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py
index 77b5e111686..561af9c9f57 100644
--- a/homeassistant/components/climate/zwave.py
+++ b/homeassistant/components/climate/zwave.py
@@ -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
diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py
index 54a221565b4..3bfc5909b0b 100644
--- a/homeassistant/components/cloud/__init__.py
+++ b/homeassistant/components/cloud/__init__.py
@@ -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):
diff --git a/homeassistant/components/cloud/auth_api.py b/homeassistant/components/cloud/auth_api.py
index dcf7567482a..042b90bf9cb 100644
--- a/homeassistant/components/cloud/auth_api.py
+++ b/homeassistant/components/cloud/auth_api.py
@@ -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
diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py
index 720ca00cf52..0df4a39406e 100644
--- a/homeassistant/components/cloud/http_api.py
+++ b/homeassistant/components/cloud/http_api.py
@@ -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):
diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py
index fb60b4075ef..ec83918e9f0 100644
--- a/homeassistant/components/config/auth.py
+++ b/homeassistant/components/config/auth.py
@@ -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,
diff --git a/homeassistant/components/cover/deconz.py b/homeassistant/components/cover/deconz.py
index 9fe65596336..89b29aa10a5 100644
--- a/homeassistant/components/cover/deconz.py
+++ b/homeassistant/components/cover/deconz.py
@@ -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)
diff --git a/homeassistant/components/cover/knx.py b/homeassistant/components/cover/knx.py
index 43a87fab367..4173db5f450 100644
--- a/homeassistant/components/cover/knx.py
+++ b/homeassistant/components/cover/knx.py
@@ -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."""
diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py
index cbc8fbee274..92a7fac1d33 100644
--- a/homeassistant/components/cover/mqtt.py
+++ b/homeassistant/components/cover/mqtt.py
@@ -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
diff --git a/homeassistant/components/cover/rflink.py b/homeassistant/components/cover/rflink.py
index 41a4c2af045..353cccc7d4f 100644
--- a/homeassistant/components/cover/rflink.py
+++ b/homeassistant/components/cover/rflink.py
@@ -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):
diff --git a/homeassistant/components/cover/ryobi_gdo.py b/homeassistant/components/cover/ryobi_gdo.py
deleted file mode 100644
index fec91f843fd..00000000000
--- a/homeassistant/components/cover/ryobi_gdo.py
+++ /dev/null
@@ -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()
diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py
index e02cdc32319..f64e4ae7a3f 100644
--- a/homeassistant/components/cover/template.py
+++ b/homeassistant/components/cover/template.py
@@ -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()
diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py
index 258087702e0..835305449e8 100644
--- a/homeassistant/components/cover/zwave.py
+++ b/homeassistant/components/cover/zwave.py
@@ -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)
diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json
index 1f7b8209089..eef2d5ce946 100644
--- a/homeassistant/components/deconz/.translations/pt.json
+++ b/homeassistant/components/deconz/.translations/pt.json
@@ -28,6 +28,6 @@
"title": "Op\u00e7\u00f5es extra de configura\u00e7\u00e3o para deCONZ"
}
},
- "title": "deCONZ"
+ "title": "Gateway Zigbee deCONZ"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py
index 617d231f92e..5462b5b61b9 100644
--- a/homeassistant/components/deconz/const.py
+++ b/homeassistant/components/deconz/const.py
@@ -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"]
diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py
index 539d4fde5ef..cf8c8e1779b 100644
--- a/homeassistant/components/device_tracker/ddwrt.py
+++ b/homeassistant/components/device_tracker/ddwrt.py
@@ -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)}
diff --git a/homeassistant/components/device_tracker/fritz.py b/homeassistant/components/device_tracker/fritz.py
index 8c9d1988a71..75e280fe908 100644
--- a/homeassistant/components/device_tracker/fritz.py
+++ b/homeassistant/components/device_tracker/fritz.py
@@ -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:
diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py
index 77f499dcf6b..aec1dcff355 100644
--- a/homeassistant/components/device_tracker/google_maps.py
+++ b/homeassistant/components/device_tracker/google_maps.py
@@ -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__)
diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py
index 2e1b96dffad..12d026a35cd 100644
--- a/homeassistant/components/device_tracker/netgear.py
+++ b/homeassistant/components/device_tracker/netgear.py
@@ -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__)
diff --git a/homeassistant/components/device_tracker/quantum_gateway.py b/homeassistant/components/device_tracker/quantum_gateway.py
new file mode 100644
index 00000000000..a06794f9179
--- /dev/null
+++ b/homeassistant/components/device_tracker/quantum_gateway.py
@@ -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)
diff --git a/homeassistant/components/device_tracker/unifi_direct.py b/homeassistant/components/device_tracker/unifi_direct.py
index 228443fe22b..3b5dcc8bac2 100644
--- a/homeassistant/components/device_tracker/unifi_direct.py
+++ b/homeassistant/components/device_tracker/unifi_direct.py
@@ -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
diff --git a/homeassistant/components/device_tracker/xiaomi_miio.py b/homeassistant/components/device_tracker/xiaomi_miio.py
index b1cfc0aed4a..d8767a4cd46 100644
--- a/homeassistant/components/device_tracker/xiaomi_miio.py
+++ b/homeassistant/components/device_tracker/xiaomi_miio.py
@@ -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
diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py
index 0640eb262cd..d7bb966a2d3 100644
--- a/homeassistant/components/discovery.py
+++ b/homeassistant/components/discovery.py
@@ -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'),
}
diff --git a/homeassistant/components/dyson.py b/homeassistant/components/dyson.py
index 3989c0bbe3e..791f990d9ad 100644
--- a/homeassistant/components/dyson.py
+++ b/homeassistant/components/dyson.py
@@ -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
diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py
new file mode 100644
index 00000000000..76594e16736
--- /dev/null
+++ b/homeassistant/components/elkm1/__init__.py
@@ -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, {})
diff --git a/homeassistant/components/elkm1/services.yaml b/homeassistant/components/elkm1/services.yaml
new file mode 100644
index 00000000000..40571656963
--- /dev/null
+++ b/homeassistant/components/elkm1/services.yaml
@@ -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
diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py
index 3e1ad2704e7..1ff04cd913a 100644
--- a/homeassistant/components/fan/mqtt.py
+++ b/homeassistant/components/fan/mqtt.py
@@ -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
diff --git a/homeassistant/components/fan/template.py b/homeassistant/components/fan/template.py
index ff25afb792a..a2f33d40e48 100644
--- a/homeassistant/components/fan/template.py
+++ b/homeassistant/components/fan/template.py
@@ -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.',
diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py
index a66e833b4b2..67a12442629 100644
--- a/homeassistant/components/fan/xiaomi_miio.py
+++ b/homeassistant/components/fan/xiaomi_miio.py
@@ -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
diff --git a/homeassistant/components/fan/zwave.py b/homeassistant/components/fan/zwave.py
index 645cb033e13..4b4204aa454 100644
--- a/homeassistant/components/fan/zwave.py
+++ b/homeassistant/components/fan/zwave.py
@@ -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)
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index df25803b4e0..0e92595ae78 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -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',
diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py
index 66753aad221..495c9e1744b 100644
--- a/homeassistant/components/geo_location/__init__.py
+++ b/homeassistant/components/geo_location/__init__.py
@@ -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
diff --git a/homeassistant/components/geo_location/demo.py b/homeassistant/components/geo_location/demo.py
index ddec369e696..6c5cc2fe147 100644
--- a/homeassistant/components/geo_location/demo.py
+++ b/homeassistant/components/geo_location/demo.py
@@ -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."""
diff --git a/homeassistant/components/geo_location/geo_json_events.py b/homeassistant/components/geo_location/geo_json_events.py
index bb17fb2450e..00ac85e6b27 100644
--- a/homeassistant/components/geo_location/geo_json_events.py
+++ b/homeassistant/components/geo_location/geo_json_events.py
@@ -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
diff --git a/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py b/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py
new file mode 100644
index 00000000000..79e0445f494
--- /dev/null
+++ b/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py
@@ -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
diff --git a/homeassistant/components/goalfeed.py b/homeassistant/components/goalfeed.py
index f360d4ffba9..1a571960bc7 100644
--- a/homeassistant/components/goalfeed.py
+++ b/homeassistant/components/goalfeed.py
@@ -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'
diff --git a/homeassistant/components/graphite.py b/homeassistant/components/graphite.py
index 2b768bc3786..26cd80d8da2 100644
--- a/homeassistant/components/graphite.py
+++ b/homeassistant/components/graphite.py
@@ -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")
diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py
index 39fd7567c98..4dd3571e69c 100644
--- a/homeassistant/components/group/__init__.py
+++ b/homeassistant/components/group/__init__.py
@@ -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)
diff --git a/homeassistant/components/group/services.yaml b/homeassistant/components/group/services.yaml
index f51f8b909d4..68c2f04f064 100644
--- a/homeassistant/components/group/services.yaml
+++ b/homeassistant/components/group/services.yaml
@@ -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.
diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py
index 44b9e392157..8e77c6bf50b 100644
--- a/homeassistant/components/habitica/__init__.py
+++ b/homeassistant/components/habitica/__init__.py
@@ -1,55 +1,54 @@
"""
The Habitica API component.
-For more details about this platform, please refer to the documentation at
+For more details about this component, please refer to the documentation at
https://home-assistant.io/components/habitica/
"""
-
-import logging
from collections import namedtuple
+import logging
import voluptuous as vol
-from homeassistant.const import \
- CONF_NAME, CONF_URL, CONF_SENSORS, CONF_PATH, CONF_API_KEY
+
+from homeassistant.const import (
+ CONF_API_KEY, CONF_NAME, CONF_PATH, CONF_SENSORS, CONF_URL)
+from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers import \
- config_validation as cv, discovery
REQUIREMENTS = ['habitipy==0.2.0']
-_LOGGER = logging.getLogger(__name__)
-DOMAIN = "habitica"
-CONF_API_USER = "api_user"
+_LOGGER = logging.getLogger(__name__)
+
+CONF_API_USER = 'api_user'
+
+DEFAULT_URL = 'https://habitica.com'
+DOMAIN = 'habitica'
ST = SensorType = namedtuple('SensorType', [
- "name", "icon", "unit", "path"
+ 'name', 'icon', 'unit', 'path'
])
SENSORS_TYPES = {
- 'name': ST('Name', None, '', ["profile", "name"]),
- 'hp': ST('HP', 'mdi:heart', 'HP', ["stats", "hp"]),
- 'maxHealth': ST('max HP', 'mdi:heart', 'HP', ["stats", "maxHealth"]),
- 'mp': ST('Mana', 'mdi:auto-fix', 'MP', ["stats", "mp"]),
- 'maxMP': ST('max Mana', 'mdi:auto-fix', 'MP', ["stats", "maxMP"]),
- 'exp': ST('EXP', 'mdi:star', 'EXP', ["stats", "exp"]),
+ 'name': ST('Name', None, '', ['profile', 'name']),
+ 'hp': ST('HP', 'mdi:heart', 'HP', ['stats', 'hp']),
+ 'maxHealth': ST('max HP', 'mdi:heart', 'HP', ['stats', 'maxHealth']),
+ 'mp': ST('Mana', 'mdi:auto-fix', 'MP', ['stats', 'mp']),
+ 'maxMP': ST('max Mana', 'mdi:auto-fix', 'MP', ['stats', 'maxMP']),
+ 'exp': ST('EXP', 'mdi:star', 'EXP', ['stats', 'exp']),
'toNextLevel': ST(
- 'Next Lvl', 'mdi:star', 'EXP', ["stats", "toNextLevel"]),
+ 'Next Lvl', 'mdi:star', 'EXP', ['stats', 'toNextLevel']),
'lvl': ST(
- 'Lvl', 'mdi:arrow-up-bold-circle-outline', 'Lvl', ["stats", "lvl"]),
- 'gp': ST('Gold', 'mdi:coin', 'Gold', ["stats", "gp"]),
- 'class': ST('Class', 'mdi:sword', '', ["stats", "class"])
+ 'Lvl', 'mdi:arrow-up-bold-circle-outline', 'Lvl', ['stats', 'lvl']),
+ 'gp': ST('Gold', 'mdi:coin', 'Gold', ['stats', 'gp']),
+ 'class': ST('Class', 'mdi:sword', '', ['stats', 'class'])
}
INSTANCE_SCHEMA = vol.Schema({
- vol.Optional(CONF_URL, default='https://habitica.com'): cv.url,
+ vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url,
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_API_USER): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_SENSORS, default=list(SENSORS_TYPES)):
- vol.All(
- cv.ensure_list,
- vol.Unique(),
- [vol.In(list(SENSORS_TYPES))])
+ vol.All(cv.ensure_list, vol.Unique(), [vol.In(list(SENSORS_TYPES))]),
})
has_unique_values = vol.Schema(vol.Unique()) # pylint: disable=invalid-name
@@ -57,7 +56,7 @@ has_unique_values = vol.Schema(vol.Unique()) # pylint: disable=invalid-name
def has_all_unique_users(value):
- """Validate that all `api_user`s are unique."""
+ """Validate that all API users are unique."""
api_users = [user[CONF_API_USER] for user in value]
has_unique_values(api_users)
return value
@@ -75,9 +74,7 @@ def has_all_unique_users_names(value):
INSTANCE_LIST_SCHEMA = vol.All(
- cv.ensure_list,
- has_all_unique_users,
- has_all_unique_users_names,
+ cv.ensure_list, has_all_unique_users, has_all_unique_users_names,
[INSTANCE_SCHEMA])
CONFIG_SCHEMA = vol.Schema({
@@ -87,23 +84,24 @@ CONFIG_SCHEMA = vol.Schema({
SERVICE_API_CALL = 'api_call'
ATTR_NAME = CONF_NAME
ATTR_PATH = CONF_PATH
-ATTR_ARGS = "args"
-EVENT_API_CALL_SUCCESS = "{0}_{1}_{2}".format(
- DOMAIN, SERVICE_API_CALL, "success")
+ATTR_ARGS = 'args'
+EVENT_API_CALL_SUCCESS = '{0}_{1}_{2}'.format(
+ DOMAIN, SERVICE_API_CALL, 'success')
SERVICE_API_CALL_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): str,
vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]),
- vol.Optional(ATTR_ARGS): dict
+ vol.Optional(ATTR_ARGS): dict,
})
async def async_setup(hass, config):
- """Set up the habitica service."""
+ """Set up the Habitica service."""
+ from habitipy.aio import HabitipyAsync
+
conf = config[DOMAIN]
data = hass.data[DOMAIN] = {}
websession = async_get_clientsession(hass)
- from habitipy.aio import HabitipyAsync
class HAHabitipyAsync(HabitipyAsync):
"""Closure API class to hold session."""
@@ -116,7 +114,7 @@ async def async_setup(hass, config):
username = instance[CONF_API_USER]
password = instance[CONF_API_KEY]
name = instance.get(CONF_NAME)
- config_dict = {"url": url, "login": username, "password": password}
+ config_dict = {'url': url, 'login': username, 'password': password}
api = HAHabitipyAsync(config_dict)
user = await api.user.get()
if name is None:
@@ -125,34 +123,30 @@ async def async_setup(hass, config):
if CONF_SENSORS in instance:
hass.async_create_task(
discovery.async_load_platform(
- hass, "sensor", DOMAIN,
- {"name": name, "sensors": instance[CONF_SENSORS]},
- config))
+ hass, 'sensor', DOMAIN,
+ {'name': name, 'sensors': instance[CONF_SENSORS]}, config))
async def handle_api_call(call):
name = call.data[ATTR_NAME]
path = call.data[ATTR_PATH]
api = hass.data[DOMAIN].get(name)
if api is None:
- _LOGGER.error(
- "API_CALL: User '%s' not configured", name)
+ _LOGGER.error("API_CALL: User '%s' not configured", name)
return
try:
for element in path:
api = api[element]
except KeyError:
_LOGGER.error(
- "API_CALL: Path %s is invalid"
- " for api on '{%s}' element", path, element)
+ "API_CALL: Path %s is invalid for API on '{%s}' element",
+ path, element)
return
kwargs = call.data.get(ATTR_ARGS, {})
data = await api(**kwargs)
- hass.bus.async_fire(EVENT_API_CALL_SUCCESS, {
- "name": name, "path": path, "data": data
- })
+ hass.bus.async_fire(
+ EVENT_API_CALL_SUCCESS, {'name': name, 'path': path, 'data': data})
hass.services.async_register(
- DOMAIN, SERVICE_API_CALL,
- handle_api_call,
+ DOMAIN, SERVICE_API_CALL, handle_api_call,
schema=SERVICE_API_CALL_SCHEMA)
return True
diff --git a/homeassistant/components/hangouts/.translations/hu.json b/homeassistant/components/hangouts/.translations/hu.json
index 2631843c784..f6e46e25985 100644
--- a/homeassistant/components/hangouts/.translations/hu.json
+++ b/homeassistant/components/hangouts/.translations/hu.json
@@ -14,6 +14,7 @@
"data": {
"2fa": "2FA Pin"
},
+ "description": "\u00dcres",
"title": "K\u00e9tfaktoros Hiteles\u00edt\u00e9s"
},
"user": {
@@ -21,6 +22,7 @@
"email": "E-Mail C\u00edm",
"password": "Jelsz\u00f3"
},
+ "description": "\u00dcres",
"title": "Google Hangouts Bejelentkez\u00e9s"
}
},
diff --git a/homeassistant/components/hangouts/.translations/pt.json b/homeassistant/components/hangouts/.translations/pt.json
index 64c960a121a..a16c60128c1 100644
--- a/homeassistant/components/hangouts/.translations/pt.json
+++ b/homeassistant/components/hangouts/.translations/pt.json
@@ -5,7 +5,7 @@
"unknown": "Ocorreu um erro desconhecido."
},
"error": {
- "invalid_2fa": "Autoriza\u00e7\u00e3o por 2 factores inv\u00e1lida, por favor, tente novamente.",
+ "invalid_2fa": "Autentica\u00e7\u00e3o por 2 fatores inv\u00e1lida, por favor, tente novamente.",
"invalid_2fa_method": "M\u00e9todo 2FA inv\u00e1lido (verificar no telefone).",
"invalid_login": "Login inv\u00e1lido, por favor, tente novamente."
},
@@ -15,7 +15,7 @@
"2fa": "Pin 2FA"
},
"description": "Vazio",
- "title": ""
+ "title": "Autentica\u00e7\u00e3o de 2 Fatores"
},
"user": {
"data": {
@@ -26,6 +26,6 @@
"title": "Login Google Hangouts"
}
},
- "title": ""
+ "title": "Google Hangouts"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/hangouts/.translations/ro.json b/homeassistant/components/hangouts/.translations/ro.json
new file mode 100644
index 00000000000..d1c3ed767ce
--- /dev/null
+++ b/homeassistant/components/hangouts/.translations/ro.json
@@ -0,0 +1,28 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Google Hangouts este deja configurat",
+ "unknown": "Sa produs o eroare necunoscut\u0103."
+ },
+ "error": {
+ "invalid_2fa_method": "Metoda 2FA invalid\u0103 (Verifica\u021bi pe telefon).",
+ "invalid_login": "Conectare invalid\u0103, \u00eencerca\u021bi din nou."
+ },
+ "step": {
+ "2fa": {
+ "data": {
+ "2fa": "2FA Pin"
+ }
+ },
+ "user": {
+ "data": {
+ "email": "Adresa de email",
+ "password": "Parol\u0103"
+ },
+ "description": "Gol",
+ "title": "Conectare Google Hangouts"
+ }
+ },
+ "title": "Google Hangouts"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py
index 5d7733584be..d4d8fe0216c 100644
--- a/homeassistant/components/homekit/__init__.py
+++ b/homeassistant/components/homekit/__init__.py
@@ -29,7 +29,7 @@ from .const import (
from .util import (
show_setup_message, validate_entity_config, validate_media_player_features)
-REQUIREMENTS = ['HAP-python==2.2.2']
+REQUIREMENTS = ['HAP-python==2.3.0']
_LOGGER = logging.getLogger(__name__)
@@ -78,7 +78,7 @@ async def async_setup(hass, config):
homekit = HomeKit(hass, name, port, ip_address, entity_filter,
entity_config)
- await hass.async_add_job(homekit.setup)
+ await hass.async_add_executor_job(homekit.setup)
if auto_start:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start)
@@ -173,6 +173,9 @@ def get_accessory(hass, driver, state, aid, config):
elif state.domain in ('automation', 'input_boolean', 'remote', 'script'):
a_type = 'Switch'
+ elif state.domain == 'water_heater':
+ a_type = 'WaterHeater'
+
if a_type is None:
return None
diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py
index adf5273b639..5baed0294b8 100644
--- a/homeassistant/components/homekit/accessories.py
+++ b/homeassistant/components/homekit/accessories.py
@@ -9,7 +9,8 @@ from pyhap.accessory_driver import AccessoryDriver
from pyhap.const import CATEGORY_OTHER
from homeassistant.const import (
- __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL)
+ __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID,
+ ATTR_SERVICE)
from homeassistant.core import callback as ha_callback
from homeassistant.core import split_entity_id
from homeassistant.helpers.event import (
@@ -17,9 +18,10 @@ from homeassistant.helpers.event import (
from homeassistant.util import dt as dt_util
from .const import (
- BRIDGE_MODEL, BRIDGE_SERIAL_NUMBER, CHAR_BATTERY_LEVEL,
- CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY, DEBOUNCE_TIMEOUT,
- MANUFACTURER, SERV_BATTERY_SERVICE)
+ ATTR_DISPLAY_NAME, ATTR_VALUE, BRIDGE_MODEL, BRIDGE_SERIAL_NUMBER,
+ CHAR_BATTERY_LEVEL, CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY,
+ DEBOUNCE_TIMEOUT, EVENT_HOMEKIT_CHANGED, MANUFACTURER,
+ SERV_BATTERY_SERVICE)
from .util import (
convert_to_float, show_setup_message, dismiss_setup_message)
@@ -33,7 +35,7 @@ def debounce(func):
"""Handle call_later callback."""
debounce_params = self.debounce.pop(func.__name__, None)
if debounce_params:
- self.hass.async_add_job(func, self, *debounce_params[1:])
+ self.hass.async_add_executor_job(func, self, *debounce_params[1:])
@wraps(func)
def wrapper(self, *args):
@@ -92,8 +94,15 @@ class HomeAccessory(Accessory):
Run inside the HAP-python event loop.
"""
+ self.hass.add_job(self.run_handler)
+
+ async def run_handler(self):
+ """Handle accessory driver started event.
+
+ Run inside the Home Assistant event loop.
+ """
state = self.hass.states.get(self.entity_id)
- self.hass.add_job(self.update_state_callback, None, None, state)
+ self.hass.async_add_job(self.update_state_callback, None, None, state)
async_track_state_change(
self.hass, self.entity_id, self.update_state_callback)
@@ -105,8 +114,8 @@ class HomeAccessory(Accessory):
if new_state is None:
return
if self._support_battery_level:
- self.hass.async_add_job(self.update_battery, new_state)
- self.hass.async_add_job(self.update_state, new_state)
+ self.hass.async_add_executor_job(self.update_battery, new_state)
+ self.hass.async_add_executor_job(self.update_state, new_state)
def update_battery(self, new_state):
"""Update battery service if available.
@@ -137,6 +146,27 @@ class HomeAccessory(Accessory):
"""
raise NotImplementedError()
+ def call_service(self, domain, service, service_data, value=None):
+ """Fire event and call service for changes from HomeKit."""
+ self.hass.add_job(
+ self.async_call_service, domain, service, service_data, value)
+
+ async def async_call_service(self, domain, service, service_data,
+ value=None):
+ """Fire event and call service for changes from HomeKit.
+
+ This method must be run in the event loop.
+ """
+ event_data = {
+ ATTR_ENTITY_ID: self.entity_id,
+ ATTR_DISPLAY_NAME: self.display_name,
+ ATTR_SERVICE: service,
+ ATTR_VALUE: value
+ }
+
+ self.hass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data)
+ await self.hass.services.async_call(domain, service, service_data)
+
class HomeBridge(Bridge):
"""Adapter class for Bridge."""
diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py
index 617dd3f4f22..d35d38c6455 100644
--- a/homeassistant/components/homekit/const.py
+++ b/homeassistant/components/homekit/const.py
@@ -5,6 +5,10 @@ DOMAIN = 'homekit'
HOMEKIT_FILE = '.homekit.state'
HOMEKIT_NOTIFY_ID = 4663548
+# #### Attributes ####
+ATTR_DISPLAY_NAME = 'display_name'
+ATTR_VALUE = 'value'
+
# #### Config ####
CONF_AUTO_START = 'auto_start'
CONF_ENTITY_CONFIG = 'entity_config'
@@ -22,6 +26,9 @@ FEATURE_PLAY_PAUSE = 'play_pause'
FEATURE_PLAY_STOP = 'play_stop'
FEATURE_TOGGLE_MUTE = 'toggle_mute'
+# #### HomeKit Component Event ####
+EVENT_HOMEKIT_CHANGED = 'homekit_state_change'
+
# #### HomeKit Component Services ####
SERVICE_HOMEKIT_START = 'start'
@@ -139,3 +146,7 @@ DEVICE_CLASS_WINDOW = 'window'
# #### Thresholds ####
THRESHOLD_CO = 25
THRESHOLD_CO2 = 1000
+
+# #### Default values ####
+DEFAULT_MIN_TEMP_WATER_HEATER = 40 # °C
+DEFAULT_MAX_TEMP_WATER_HEATER = 60 # °C
diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py
index cf0620a4e30..840800f730b 100644
--- a/homeassistant/components/homekit/type_covers.py
+++ b/homeassistant/components/homekit/type_covers.py
@@ -31,7 +31,7 @@ class GarageDoorOpener(HomeAccessory):
def __init__(self, *args):
"""Initialize a GarageDoorOpener accessory object."""
super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER)
- self.flag_target_state = False
+ self._flag_state = False
serv_garage_door = self.add_preload_service(SERV_GARAGE_DOOR_OPENER)
self.char_current_state = serv_garage_door.configure_char(
@@ -42,15 +42,15 @@ class GarageDoorOpener(HomeAccessory):
def set_state(self, value):
"""Change garage state if call came from HomeKit."""
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
- self.flag_target_state = True
+ self._flag_state = True
params = {ATTR_ENTITY_ID: self.entity_id}
if value == 0:
self.char_current_state.set_value(3)
- self.hass.services.call(DOMAIN, SERVICE_OPEN_COVER, params)
+ self.call_service(DOMAIN, SERVICE_OPEN_COVER, params)
elif value == 1:
self.char_current_state.set_value(2)
- self.hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, params)
+ self.call_service(DOMAIN, SERVICE_CLOSE_COVER, params)
def update_state(self, new_state):
"""Update cover state after state changed."""
@@ -58,9 +58,9 @@ class GarageDoorOpener(HomeAccessory):
if hass_state in (STATE_OPEN, STATE_CLOSED):
current_state = 0 if hass_state == STATE_OPEN else 1
self.char_current_state.set_value(current_state)
- if not self.flag_target_state:
+ if not self._flag_state:
self.char_target_state.set_value(current_state)
- self.flag_target_state = False
+ self._flag_state = False
@TYPES.register('WindowCovering')
@@ -73,7 +73,7 @@ class WindowCovering(HomeAccessory):
def __init__(self, *args):
"""Initialize a WindowCovering accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
- self.homekit_target = None
+ self._homekit_target = None
serv_cover = self.add_preload_service(SERV_WINDOW_COVERING)
self.char_current_position = serv_cover.configure_char(
@@ -85,20 +85,20 @@ class WindowCovering(HomeAccessory):
def move_cover(self, value):
"""Move cover to value if call came from HomeKit."""
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
- self.homekit_target = value
+ self._homekit_target = value
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_POSITION: value}
- self.hass.services.call(DOMAIN, SERVICE_SET_COVER_POSITION, params)
+ self.call_service(DOMAIN, SERVICE_SET_COVER_POSITION, params, value)
def update_state(self, new_state):
"""Update cover position after state changed."""
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
if isinstance(current_position, int):
self.char_current_position.set_value(current_position)
- if self.homekit_target is None or \
- abs(current_position - self.homekit_target) < 6:
+ if self._homekit_target is None or \
+ abs(current_position - self._homekit_target) < 6:
self.char_target_position.set_value(current_position)
- self.homekit_target = None
+ self._homekit_target = None
@TYPES.register('WindowCoveringBasic')
@@ -114,7 +114,7 @@ class WindowCoveringBasic(HomeAccessory):
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
features = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_SUPPORTED_FEATURES)
- self.supports_stop = features & SUPPORT_STOP
+ self._supports_stop = features & SUPPORT_STOP
serv_cover = self.add_preload_service(SERV_WINDOW_COVERING)
self.char_current_position = serv_cover.configure_char(
@@ -129,7 +129,7 @@ class WindowCoveringBasic(HomeAccessory):
"""Move cover to value if call came from HomeKit."""
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
- if self.supports_stop:
+ if self._supports_stop:
if value > 70:
service, position = (SERVICE_OPEN_COVER, 100)
elif value < 30:
@@ -143,7 +143,7 @@ class WindowCoveringBasic(HomeAccessory):
service, position = (SERVICE_CLOSE_COVER, 0)
params = {ATTR_ENTITY_ID: self.entity_id}
- self.hass.services.call(DOMAIN, service, params)
+ self.call_service(DOMAIN, service, params)
# Snap the current/target position to the expected final position.
self.char_current_position.set_value(position)
diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py
index aa44b11fefb..5a860ed21c8 100644
--- a/homeassistant/components/homekit/type_fans.py
+++ b/homeassistant/components/homekit/type_fans.py
@@ -34,24 +34,27 @@ class Fan(HomeAccessory):
CHAR_SWING_MODE: False}
self._state = 0
- self.chars = []
+ chars = []
features = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_SUPPORTED_FEATURES)
if features & SUPPORT_DIRECTION:
- self.chars.append(CHAR_ROTATION_DIRECTION)
+ chars.append(CHAR_ROTATION_DIRECTION)
if features & SUPPORT_OSCILLATE:
- self.chars.append(CHAR_SWING_MODE)
+ chars.append(CHAR_SWING_MODE)
- serv_fan = self.add_preload_service(SERV_FANV2, self.chars)
+ serv_fan = self.add_preload_service(SERV_FANV2, chars)
self.char_active = serv_fan.configure_char(
CHAR_ACTIVE, value=0, setter_callback=self.set_state)
- if CHAR_ROTATION_DIRECTION in self.chars:
+ self.char_direction = None
+ self.char_swing = None
+
+ if CHAR_ROTATION_DIRECTION in chars:
self.char_direction = serv_fan.configure_char(
CHAR_ROTATION_DIRECTION, value=0,
setter_callback=self.set_direction)
- if CHAR_SWING_MODE in self.chars:
+ if CHAR_SWING_MODE in chars:
self.char_swing = serv_fan.configure_char(
CHAR_SWING_MODE, value=0, setter_callback=self.set_oscillating)
@@ -61,7 +64,7 @@ class Fan(HomeAccessory):
self._flag[CHAR_ACTIVE] = True
service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF
params = {ATTR_ENTITY_ID: self.entity_id}
- self.hass.services.call(DOMAIN, service, params)
+ self.call_service(DOMAIN, service, params)
def set_direction(self, value):
"""Set state if call came from HomeKit."""
@@ -69,7 +72,7 @@ class Fan(HomeAccessory):
self._flag[CHAR_ROTATION_DIRECTION] = True
direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_DIRECTION: direction}
- self.hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, params)
+ self.call_service(DOMAIN, SERVICE_SET_DIRECTION, params, direction)
def set_oscillating(self, value):
"""Set state if call came from HomeKit."""
@@ -78,7 +81,7 @@ class Fan(HomeAccessory):
oscillating = True if value == 1 else False
params = {ATTR_ENTITY_ID: self.entity_id,
ATTR_OSCILLATING: oscillating}
- self.hass.services.call(DOMAIN, SERVICE_OSCILLATE, params)
+ self.call_service(DOMAIN, SERVICE_OSCILLATE, params, oscillating)
def update_state(self, new_state):
"""Update fan after state change."""
@@ -92,7 +95,7 @@ class Fan(HomeAccessory):
self._flag[CHAR_ACTIVE] = False
# Handle Direction
- if CHAR_ROTATION_DIRECTION in self.chars:
+ if self.char_direction is not None:
direction = new_state.attributes.get(ATTR_DIRECTION)
if not self._flag[CHAR_ROTATION_DIRECTION] and \
direction in (DIRECTION_FORWARD, DIRECTION_REVERSE):
@@ -102,7 +105,7 @@ class Fan(HomeAccessory):
self._flag[CHAR_ROTATION_DIRECTION] = False
# Handle Oscillating
- if CHAR_SWING_MODE in self.chars:
+ if self.char_swing is not None:
oscillating = new_state.attributes.get(ATTR_OSCILLATING)
if not self._flag[CHAR_SWING_MODE] and \
oscillating in (True, False):
diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py
index da012799602..a9007ace35b 100644
--- a/homeassistant/components/homekit/type_lights.py
+++ b/homeassistant/components/homekit/type_lights.py
@@ -83,7 +83,7 @@ class Light(HomeAccessory):
self._flag[CHAR_ON] = True
params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF
- self.hass.services.call(DOMAIN, service, params)
+ self.call_service(DOMAIN, service, params)
@debounce
def set_brightness(self, value):
@@ -94,14 +94,16 @@ class Light(HomeAccessory):
self.set_state(0) # Turn off light
return
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value}
- self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
+ self.call_service(DOMAIN, SERVICE_TURN_ON, params,
+ 'brightness at {}%'.format(value))
def set_color_temperature(self, value):
"""Set color temperature if call came from HomeKit."""
_LOGGER.debug('%s: Set color temp to %s', self.entity_id, value)
self._flag[CHAR_COLOR_TEMPERATURE] = True
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value}
- self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
+ self.call_service(DOMAIN, SERVICE_TURN_ON, params,
+ 'color temperature at {}'.format(value))
def set_saturation(self, value):
"""Set saturation if call came from HomeKit."""
@@ -126,7 +128,8 @@ class Light(HomeAccessory):
self._flag.update({
CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True})
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color}
- self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
+ self.call_service(DOMAIN, SERVICE_TURN_ON, params,
+ 'set color at {}'.format(color))
def update_state(self, new_state):
"""Update light after state change."""
diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py
index 05ab6c6f822..fb211617ecf 100644
--- a/homeassistant/components/homekit/type_locks.py
+++ b/homeassistant/components/homekit/type_locks.py
@@ -33,7 +33,7 @@ class Lock(HomeAccessory):
"""Initialize a Lock accessory object."""
super().__init__(*args, category=CATEGORY_DOOR_LOCK)
self._code = self.config.get(ATTR_CODE)
- self.flag_target_state = False
+ self._flag_state = False
serv_lock_mechanism = self.add_preload_service(SERV_LOCK)
self.char_current_state = serv_lock_mechanism.configure_char(
@@ -45,8 +45,8 @@ class Lock(HomeAccessory):
def set_state(self, value):
"""Set lock state to value if call came from HomeKit."""
- _LOGGER.debug("%s: Set state to %d", self.entity_id, value)
- self.flag_target_state = True
+ _LOGGER.debug('%s: Set state to %d', self.entity_id, value)
+ self._flag_state = True
hass_value = HOMEKIT_TO_HASS.get(value)
service = STATE_TO_SERVICE[hass_value]
@@ -54,7 +54,7 @@ class Lock(HomeAccessory):
params = {ATTR_ENTITY_ID: self.entity_id}
if self._code:
params[ATTR_CODE] = self._code
- self.hass.services.call(DOMAIN, service, params)
+ self.call_service(DOMAIN, service, params)
def update_state(self, new_state):
"""Update lock after state changed."""
@@ -67,6 +67,6 @@ class Lock(HomeAccessory):
# LockTargetState only supports locked and unlocked
if hass_state in (STATE_LOCKED, STATE_UNLOCKED):
- if not self.flag_target_state:
+ if not self._flag_state:
self.char_target_state.set_value(current_lock_state)
- self.flag_target_state = False
+ self._flag_state = False
diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py
index ec41b9fd618..09088871fd2 100644
--- a/homeassistant/components/homekit/type_media_players.py
+++ b/homeassistant/components/homekit/type_media_players.py
@@ -76,7 +76,7 @@ class MediaPlayer(HomeAccessory):
self._flag[FEATURE_ON_OFF] = True
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
params = {ATTR_ENTITY_ID: self.entity_id}
- self.hass.services.call(DOMAIN, service, params)
+ self.call_service(DOMAIN, service, params)
def set_play_pause(self, value):
"""Move switch state to value if call came from HomeKit."""
@@ -85,7 +85,7 @@ class MediaPlayer(HomeAccessory):
self._flag[FEATURE_PLAY_PAUSE] = True
service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_PAUSE
params = {ATTR_ENTITY_ID: self.entity_id}
- self.hass.services.call(DOMAIN, service, params)
+ self.call_service(DOMAIN, service, params)
def set_play_stop(self, value):
"""Move switch state to value if call came from HomeKit."""
@@ -94,7 +94,7 @@ class MediaPlayer(HomeAccessory):
self._flag[FEATURE_PLAY_STOP] = True
service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_STOP
params = {ATTR_ENTITY_ID: self.entity_id}
- self.hass.services.call(DOMAIN, service, params)
+ self.call_service(DOMAIN, service, params)
def set_toggle_mute(self, value):
"""Move switch state to value if call came from HomeKit."""
@@ -103,7 +103,7 @@ class MediaPlayer(HomeAccessory):
self._flag[FEATURE_TOGGLE_MUTE] = True
params = {ATTR_ENTITY_ID: self.entity_id,
ATTR_MEDIA_VOLUME_MUTED: value}
- self.hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, params)
+ self.call_service(DOMAIN, SERVICE_VOLUME_MUTE, params)
def update_state(self, new_state):
"""Update switch state after state changed."""
diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py
index a7d36720cab..e210217df2f 100644
--- a/homeassistant/components/homekit/type_security_systems.py
+++ b/homeassistant/components/homekit/type_security_systems.py
@@ -39,7 +39,7 @@ class SecuritySystem(HomeAccessory):
"""Initialize a SecuritySystem accessory object."""
super().__init__(*args, category=CATEGORY_ALARM_SYSTEM)
self._alarm_code = self.config.get(ATTR_CODE)
- self.flag_target_state = False
+ self._flag_state = False
serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM)
self.char_current_state = serv_alarm.configure_char(
@@ -52,14 +52,14 @@ class SecuritySystem(HomeAccessory):
"""Move security state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set security state to %d',
self.entity_id, value)
- self.flag_target_state = True
+ self._flag_state = True
hass_value = HOMEKIT_TO_HASS[value]
service = STATE_TO_SERVICE[hass_value]
params = {ATTR_ENTITY_ID: self.entity_id}
if self._alarm_code:
params[ATTR_CODE] = self._alarm_code
- self.hass.services.call(DOMAIN, service, params)
+ self.call_service(DOMAIN, service, params)
def update_state(self, new_state):
"""Update security state after state changed."""
@@ -71,7 +71,7 @@ class SecuritySystem(HomeAccessory):
self.entity_id, hass_state, current_security_state)
# SecuritySystemTargetState does not support triggered
- if not self.flag_target_state and \
+ if not self._flag_state and \
hass_state != STATE_ALARM_TRIGGERED:
self.char_target_state.set_value(current_security_state)
- self.flag_target_state = False
+ self._flag_state = False
diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py
index d2101b1e6f9..09da361ddb8 100644
--- a/homeassistant/components/homekit/type_sensors.py
+++ b/homeassistant/components/homekit/type_sensors.py
@@ -58,7 +58,6 @@ class TemperatureSensor(HomeAccessory):
serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR)
self.char_temp = serv_temp.configure_char(
CHAR_CURRENT_TEMPERATURE, value=0, properties=PROP_CELSIUS)
- self.unit = None
def update_state(self, new_state):
"""Update temperature after state changed."""
diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py
index 82a5d68d644..839abe5a580 100644
--- a/homeassistant/components/homekit/type_switches.py
+++ b/homeassistant/components/homekit/type_switches.py
@@ -1,7 +1,9 @@
"""Class to hold all switch accessories."""
import logging
-from pyhap.const import CATEGORY_OUTLET, CATEGORY_SWITCH
+from pyhap.const import (
+ CATEGORY_FAUCET, CATEGORY_OUTLET, CATEGORY_SHOWER_HEAD,
+ CATEGORY_SPRINKLER, CATEGORY_SWITCH)
from homeassistant.components.switch import DOMAIN
from homeassistant.const import (
@@ -17,10 +19,6 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
-CATEGORY_SPRINKLER = 28
-CATEGORY_FAUCET = 29
-CATEGORY_SHOWER_HEAD = 30
-
VALVE_TYPE = {
TYPE_FAUCET: (CATEGORY_FAUCET, 3),
TYPE_SHOWER: (CATEGORY_SHOWER_HEAD, 2),
@@ -36,7 +34,7 @@ class Outlet(HomeAccessory):
def __init__(self, *args):
"""Initialize an Outlet accessory object."""
super().__init__(*args, category=CATEGORY_OUTLET)
- self.flag_target_state = False
+ self._flag_state = False
serv_outlet = self.add_preload_service(SERV_OUTLET)
self.char_on = serv_outlet.configure_char(
@@ -48,19 +46,19 @@ class Outlet(HomeAccessory):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state to %s',
self.entity_id, value)
- self.flag_target_state = True
+ self._flag_state = True
params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
- self.hass.services.call(DOMAIN, service, params)
+ self.call_service(DOMAIN, service, params)
def update_state(self, new_state):
"""Update switch state after state changed."""
current_state = (new_state.state == STATE_ON)
- if not self.flag_target_state:
+ if not self._flag_state:
_LOGGER.debug('%s: Set current state to %s',
self.entity_id, current_state)
self.char_on.set_value(current_state)
- self.flag_target_state = False
+ self._flag_state = False
@TYPES.register('Switch')
@@ -71,7 +69,7 @@ class Switch(HomeAccessory):
"""Initialize a Switch accessory object."""
super().__init__(*args, category=CATEGORY_SWITCH)
self._domain = split_entity_id(self.entity_id)[0]
- self.flag_target_state = False
+ self._flag_state = False
serv_switch = self.add_preload_service(SERV_SWITCH)
self.char_on = serv_switch.configure_char(
@@ -81,19 +79,19 @@ class Switch(HomeAccessory):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state to %s',
self.entity_id, value)
- self.flag_target_state = True
+ self._flag_state = True
params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
- self.hass.services.call(self._domain, service, params)
+ self.call_service(self._domain, service, params)
def update_state(self, new_state):
"""Update switch state after state changed."""
current_state = (new_state.state == STATE_ON)
- if not self.flag_target_state:
+ if not self._flag_state:
_LOGGER.debug('%s: Set current state to %s',
self.entity_id, current_state)
self.char_on.set_value(current_state)
- self.flag_target_state = False
+ self._flag_state = False
@TYPES.register('Valve')
@@ -103,7 +101,7 @@ class Valve(HomeAccessory):
def __init__(self, *args):
"""Initialize a Valve accessory object."""
super().__init__(*args)
- self.flag_target_state = False
+ self._flag_state = False
valve_type = self.config[CONF_TYPE]
self.category = VALVE_TYPE[valve_type][0]
@@ -119,18 +117,18 @@ class Valve(HomeAccessory):
"""Move value state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state to %s',
self.entity_id, value)
- self.flag_target_state = True
+ self._flag_state = True
self.char_in_use.set_value(value)
params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
- self.hass.services.call(DOMAIN, service, params)
+ self.call_service(DOMAIN, service, params)
def update_state(self, new_state):
"""Update switch state after state changed."""
current_state = (new_state.state == STATE_ON)
- if not self.flag_target_state:
+ if not self._flag_state:
_LOGGER.debug('%s: Set current state to %s',
self.entity_id, current_state)
self.char_active.set_value(current_state)
self.char_in_use.set_value(current_state)
- self.flag_target_state = False
+ self._flag_state = False
diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py
index 8517122f6a8..49da6db6125 100644
--- a/homeassistant/components/homekit/type_thermostats.py
+++ b/homeassistant/components/homekit/type_thermostats.py
@@ -6,13 +6,18 @@ from pyhap.const import CATEGORY_THERMOSTAT
from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP,
ATTR_OPERATION_LIST, ATTR_OPERATION_MODE,
- ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
- DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP,
- DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO,
- STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH,
- SUPPORT_TARGET_TEMPERATURE_LOW)
+ ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
+ DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN as DOMAIN_CLIMATE,
+ SERVICE_SET_OPERATION_MODE as SERVICE_SET_OPERATION_MODE_THERMOSTAT,
+ SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT,
+ STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF,
+ SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
+from homeassistant.components.water_heater import (
+ DOMAIN as DOMAIN_WATER_HEATER,
+ SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_WATER_HEATER)
from homeassistant.const import (
- ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON,
+ ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE,
+ SERVICE_TURN_OFF, SERVICE_TURN_ON,
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from . import TYPES
@@ -21,7 +26,9 @@ from .const import (
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING,
CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING,
CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE,
- CHAR_TEMP_DISPLAY_UNITS, PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_THERMOSTAT)
+ CHAR_TEMP_DISPLAY_UNITS,
+ DEFAULT_MAX_TEMP_WATER_HEATER, DEFAULT_MIN_TEMP_WATER_HEATER,
+ PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_THERMOSTAT)
from .util import temperature_to_homekit, temperature_to_states
_LOGGER = logging.getLogger(__name__)
@@ -44,11 +51,11 @@ class Thermostat(HomeAccessory):
"""Initialize a Thermostat accessory object."""
super().__init__(*args, category=CATEGORY_THERMOSTAT)
self._unit = self.hass.config.units.temperature_unit
+ self._flag_heat_cool = False
+ self._flag_temperature = False
+ self._flag_coolingthresh = False
+ self._flag_heatingthresh = False
self.support_power_state = False
- self.heat_cool_flag_target_state = False
- self.temperature_flag_target_state = False
- self.coolingthresh_flag_target_state = False
- self.heatingthresh_flag_target_state = False
min_temp, max_temp = self.get_temperature_range()
# Add additional characteristics if auto mode is supported
@@ -114,60 +121,70 @@ class Thermostat(HomeAccessory):
return min_temp, max_temp
def set_heat_cool(self, value):
- """Move operation mode to value if call came from HomeKit."""
- if value in HC_HOMEKIT_TO_HASS:
- _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
- self.heat_cool_flag_target_state = True
- hass_value = HC_HOMEKIT_TO_HASS[value]
- if self.support_power_state is True:
- params = {ATTR_ENTITY_ID: self.entity_id}
- if hass_value == STATE_OFF:
- self.hass.services.call(DOMAIN, SERVICE_TURN_OFF, params)
- return
- self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
- params = {ATTR_ENTITY_ID: self.entity_id,
- ATTR_OPERATION_MODE: hass_value}
- self.hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, params)
+ """Change operation mode to value if call came from HomeKit."""
+ _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
+ self._flag_heat_cool = True
+ hass_value = HC_HOMEKIT_TO_HASS[value]
+ if self.support_power_state is True:
+ params = {ATTR_ENTITY_ID: self.entity_id}
+ if hass_value == STATE_OFF:
+ self.call_service(DOMAIN_CLIMATE, SERVICE_TURN_OFF, params)
+ return
+ self.call_service(DOMAIN_CLIMATE, SERVICE_TURN_ON, params)
+ params = {ATTR_ENTITY_ID: self.entity_id,
+ ATTR_OPERATION_MODE: hass_value}
+ self.call_service(
+ DOMAIN_CLIMATE, SERVICE_SET_OPERATION_MODE_THERMOSTAT,
+ params, hass_value)
@debounce
def set_cooling_threshold(self, value):
"""Set cooling threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C',
self.entity_id, value)
- self.coolingthresh_flag_target_state = True
+ self._flag_coolingthresh = True
low = self.char_heating_thresh_temp.value
+ temperature = temperature_to_states(value, self._unit)
params = {
ATTR_ENTITY_ID: self.entity_id,
- ATTR_TARGET_TEMP_HIGH: temperature_to_states(value, self._unit),
+ ATTR_TARGET_TEMP_HIGH: temperature,
ATTR_TARGET_TEMP_LOW: temperature_to_states(low, self._unit)}
- self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params)
+ self.call_service(
+ DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT,
+ params, 'cooling threshold {}{}'.format(temperature, self._unit))
@debounce
def set_heating_threshold(self, value):
"""Set heating threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
self.entity_id, value)
- self.heatingthresh_flag_target_state = True
+ self._flag_heatingthresh = True
high = self.char_cooling_thresh_temp.value
+ temperature = temperature_to_states(value, self._unit)
params = {
ATTR_ENTITY_ID: self.entity_id,
ATTR_TARGET_TEMP_HIGH: temperature_to_states(high, self._unit),
- ATTR_TARGET_TEMP_LOW: temperature_to_states(value, self._unit)}
- self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params)
+ ATTR_TARGET_TEMP_LOW: temperature}
+ self.call_service(
+ DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT,
+ params, 'heating threshold {}{}'.format(temperature, self._unit))
@debounce
def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit."""
_LOGGER.debug('%s: Set target temperature to %.2f°C',
self.entity_id, value)
- self.temperature_flag_target_state = True
+ self._flag_temperature = True
+ temperature = temperature_to_states(value, self._unit)
params = {
ATTR_ENTITY_ID: self.entity_id,
- ATTR_TEMPERATURE: temperature_to_states(value, self._unit)}
- self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params)
+ ATTR_TEMPERATURE: temperature}
+ self.call_service(
+ DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT,
+ params, '{}{}'.format(temperature, self._unit))
def update_state(self, new_state):
- """Update security state after state changed."""
+ """Update thermostat state after state changed."""
# Update current temperature
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
if isinstance(current_temp, (int, float)):
@@ -178,9 +195,9 @@ class Thermostat(HomeAccessory):
target_temp = new_state.attributes.get(ATTR_TEMPERATURE)
if isinstance(target_temp, (int, float)):
target_temp = temperature_to_homekit(target_temp, self._unit)
- if not self.temperature_flag_target_state:
+ if not self._flag_temperature:
self.char_target_temp.set_value(target_temp)
- self.temperature_flag_target_state = False
+ self._flag_temperature = False
# Update cooling threshold temperature if characteristic exists
if self.char_cooling_thresh_temp:
@@ -188,9 +205,9 @@ class Thermostat(HomeAccessory):
if isinstance(cooling_thresh, (int, float)):
cooling_thresh = temperature_to_homekit(cooling_thresh,
self._unit)
- if not self.coolingthresh_flag_target_state:
+ if not self._flag_coolingthresh:
self.char_cooling_thresh_temp.set_value(cooling_thresh)
- self.coolingthresh_flag_target_state = False
+ self._flag_coolingthresh = False
# Update heating threshold temperature if characteristic exists
if self.char_heating_thresh_temp:
@@ -198,9 +215,9 @@ class Thermostat(HomeAccessory):
if isinstance(heating_thresh, (int, float)):
heating_thresh = temperature_to_homekit(heating_thresh,
self._unit)
- if not self.heatingthresh_flag_target_state:
+ if not self._flag_heatingthresh:
self.char_heating_thresh_temp.set_value(heating_thresh)
- self.heatingthresh_flag_target_state = False
+ self._flag_heatingthresh = False
# Update display units
if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT:
@@ -209,13 +226,12 @@ class Thermostat(HomeAccessory):
# Update target operation mode
operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE)
if self.support_power_state is True and new_state.state == STATE_OFF:
- self.char_target_heat_cool.set_value(
- HC_HASS_TO_HOMEKIT[STATE_OFF])
+ self.char_target_heat_cool.set_value(0) # Off
elif operation_mode and operation_mode in HC_HASS_TO_HOMEKIT:
- if not self.heat_cool_flag_target_state:
+ if not self._flag_heat_cool:
self.char_target_heat_cool.set_value(
HC_HASS_TO_HOMEKIT[operation_mode])
- self.heat_cool_flag_target_state = False
+ self._flag_heat_cool = False
# Set current operation mode based on temperatures and target mode
if self.support_power_state is True and new_state.state == STATE_OFF:
@@ -258,3 +274,92 @@ class Thermostat(HomeAccessory):
self.char_current_heat_cool.set_value(
HC_HASS_TO_HOMEKIT[current_operation_mode])
+
+
+@TYPES.register('WaterHeater')
+class WaterHeater(HomeAccessory):
+ """Generate a WaterHeater accessory for a water_heater."""
+
+ def __init__(self, *args):
+ """Initialize a WaterHeater accessory object."""
+ super().__init__(*args, category=CATEGORY_THERMOSTAT)
+ self._unit = self.hass.config.units.temperature_unit
+ self._flag_heat_cool = False
+ self._flag_temperature = False
+ min_temp, max_temp = self.get_temperature_range()
+
+ serv_thermostat = self.add_preload_service(SERV_THERMOSTAT)
+
+ self.char_current_heat_cool = serv_thermostat.configure_char(
+ CHAR_CURRENT_HEATING_COOLING, value=1)
+ self.char_target_heat_cool = serv_thermostat.configure_char(
+ CHAR_TARGET_HEATING_COOLING, value=1,
+ setter_callback=self.set_heat_cool)
+
+ self.char_current_temp = serv_thermostat.configure_char(
+ CHAR_CURRENT_TEMPERATURE, value=50.0)
+ self.char_target_temp = serv_thermostat.configure_char(
+ CHAR_TARGET_TEMPERATURE, value=50.0,
+ properties={PROP_MIN_VALUE: min_temp,
+ PROP_MAX_VALUE: max_temp},
+ setter_callback=self.set_target_temperature)
+
+ self.char_display_units = serv_thermostat.configure_char(
+ CHAR_TEMP_DISPLAY_UNITS, value=0)
+
+ def get_temperature_range(self):
+ """Return min and max temperature range."""
+ max_temp = self.hass.states.get(self.entity_id) \
+ .attributes.get(ATTR_MAX_TEMP)
+ max_temp = temperature_to_homekit(max_temp, self._unit) if max_temp \
+ else DEFAULT_MAX_TEMP_WATER_HEATER
+
+ min_temp = self.hass.states.get(self.entity_id) \
+ .attributes.get(ATTR_MIN_TEMP)
+ min_temp = temperature_to_homekit(min_temp, self._unit) if min_temp \
+ else DEFAULT_MIN_TEMP_WATER_HEATER
+
+ return min_temp, max_temp
+
+ def set_heat_cool(self, value):
+ """Change operation mode to value if call came from HomeKit."""
+ _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
+ self._flag_heat_cool = True
+ hass_value = HC_HOMEKIT_TO_HASS[value]
+ if hass_value != STATE_HEAT:
+ self.char_target_heat_cool.set_value(1) # Heat
+
+ @debounce
+ def set_target_temperature(self, value):
+ """Set target temperature to value if call came from HomeKit."""
+ _LOGGER.debug('%s: Set target temperature to %.2f°C',
+ self.entity_id, value)
+ self._flag_temperature = True
+ temperature = temperature_to_states(value, self._unit)
+ params = {
+ ATTR_ENTITY_ID: self.entity_id,
+ ATTR_TEMPERATURE: temperature}
+ self.call_service(
+ DOMAIN_WATER_HEATER, SERVICE_SET_TEMPERATURE_WATER_HEATER,
+ params, '{}{}'.format(temperature, self._unit))
+
+ def update_state(self, new_state):
+ """Update water_heater state after state change."""
+ # Update current and target temperature
+ temperature = new_state.attributes.get(ATTR_TEMPERATURE)
+ if isinstance(temperature, (int, float)):
+ temperature = temperature_to_homekit(temperature, self._unit)
+ self.char_current_temp.set_value(temperature)
+ if not self._flag_temperature:
+ self.char_target_temp.set_value(temperature)
+ self._flag_temperature = False
+
+ # Update display units
+ if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT:
+ self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit])
+
+ # Update target operation mode
+ operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE)
+ if operation_mode and not self._flag_heat_cool:
+ self.char_target_heat_cool.set_value(1) # Heat
+ self._flag_heat_cool = False
diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py
index 4dd7396cf8d..43ae4df3b50 100644
--- a/homeassistant/components/homekit/util.py
+++ b/homeassistant/components/homekit/util.py
@@ -104,7 +104,7 @@ def validate_media_player_features(state, feature_list):
error_list.append(feature)
if error_list:
- _LOGGER.error("%s does not support features: %s",
+ _LOGGER.error('%s does not support features: %s',
state.entity_id, error_list)
return False
return True
diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py
index 5431dd4a61a..ebb4a2db9cb 100644
--- a/homeassistant/components/homekit_controller/__init__.py
+++ b/homeassistant/components/homekit_controller/__init__.py
@@ -65,8 +65,7 @@ def homekit_http_send(self, message_body=None, encode_chunked=False):
def get_serial(accessory):
"""Obtain the serial number of a HomeKit device."""
- # pylint: disable=import-error
- import homekit
+ import homekit # pylint: disable=import-error
for service in accessory['services']:
if homekit.ServicesTypes.get_short(service['type']) != \
'accessory-information':
@@ -85,8 +84,7 @@ class HKDevice():
def __init__(self, hass, host, port, model, hkid, config_num, config):
"""Initialise a generic HomeKit device."""
- # pylint: disable=import-error
- import homekit
+ import homekit # pylint: disable=import-error
_LOGGER.info("Setting up Homekit device %s", model)
self.hass = hass
@@ -132,8 +130,7 @@ class HKDevice():
def accessory_setup(self):
"""Handle setup of a HomeKit accessory."""
- # pylint: disable=import-error
- import homekit
+ import homekit # pylint: disable=import-error
try:
data = self.get_json('/accessories')
@@ -185,8 +182,7 @@ class HKDevice():
def device_config_callback(self, callback_data):
"""Handle initial pairing."""
- # pylint: disable=import-error
- import homekit
+ import homekit # pylint: disable=import-error
pairing_id = str(uuid.uuid4())
code = callback_data.get('code').strip()
try:
diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py
index 927f86b590d..4343bcfbc08 100644
--- a/homeassistant/components/homematic/__init__.py
+++ b/homeassistant/components/homematic/__init__.py
@@ -18,7 +18,7 @@ from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['pyhomematic==0.1.50']
+REQUIREMENTS = ['pyhomematic==0.1.51']
_LOGGER = logging.getLogger(__name__)
@@ -76,7 +76,7 @@ HM_DEVICE_TYPES = {
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat',
'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor',
'IPKeySwitchPowermeter', 'IPThermostatWall230V', 'IPWeatherSensorPlus',
- 'IPWeatherSensorBasic'],
+ 'IPWeatherSensorBasic', 'IPBrightnessSensor'],
DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
@@ -97,17 +97,21 @@ HM_IGNORE_DISCOVERY_NODE = [
]
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = {
- 'ACTUAL_TEMPERATURE': ['IPAreaThermostat', 'IPWeatherSensor'],
+ 'ACTUAL_TEMPERATURE': [
+ 'IPAreaThermostat', 'IPWeatherSensor',
+ 'IPWeatherSensorPlus', 'IPWeatherSensorBasic'],
}
HM_ATTRIBUTE_SUPPORT = {
'LOWBAT': ['battery', {0: 'High', 1: 'Low'}],
'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}],
'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}],
+ 'ERROR_SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}],
'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}],
'RSSI_PEER': ['rssi', {}],
'RSSI_DEVICE': ['rssi', {}],
'VALVE_STATE': ['valve', {}],
+ 'LEVEL': ['level', {}],
'BATTERY_STATE': ['battery', {}],
'CONTROL_MODE': ['mode', {
0: 'Auto',
@@ -775,8 +779,7 @@ class HMDevice(Entity):
# Link events from pyhomematic
self._subscribe_homematic_events()
self._available = not self._hmdevice.UNREACH
- # pylint: disable=broad-except
- except Exception as err:
+ except Exception as err: # pylint: disable=broad-except
self._connected = False
_LOGGER.error("Exception while linking %s: %s",
self._address, str(err))
@@ -795,11 +798,8 @@ class HMDevice(Entity):
has_changed = True
# Availability has changed
- if attribute == 'UNREACH':
- self._available = not bool(value)
- has_changed = True
- elif not self.available:
- self._available = False
+ if self.available != (not self._hmdevice.UNREACH):
+ self._available = not self._hmdevice.UNREACH
has_changed = True
# If it has changed data point, update HASS
@@ -809,7 +809,6 @@ class HMDevice(Entity):
def _subscribe_homematic_events(self):
"""Subscribe all required events to handle job."""
channels_to_sub = set()
- channels_to_sub.add(0) # Add channel 0 for UNREACH
# Push data to channels_to_sub from hmdevice metadata
for metadata in (self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE,
diff --git a/homeassistant/components/homematicip_cloud/.translations/ro.json b/homeassistant/components/homematicip_cloud/.translations/ro.json
new file mode 100644
index 00000000000..a5399e7e68c
--- /dev/null
+++ b/homeassistant/components/homematicip_cloud/.translations/ro.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Punctul de acces este deja configurat",
+ "unknown": "Sa produs o eroare necunoscut\u0103."
+ },
+ "error": {
+ "invalid_pin": "Cod PIN invalid, \u00eencerca\u021bi din nou.",
+ "press_the_button": "V\u0103 rug\u0103m s\u0103 ap\u0103sa\u021bi butonul albastru."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "pin": "Cod PIN (op\u021bional)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huawei_lte.py b/homeassistant/components/huawei_lte.py
index ad134d8c60e..e8d3bc8b3a1 100644
--- a/homeassistant/components/huawei_lte.py
+++ b/homeassistant/components/huawei_lte.py
@@ -21,6 +21,9 @@ from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
+# dicttoxml (used by huawei-lte-api) has uselessly verbose INFO level.
+logging.getLogger('dicttoxml').setLevel(logging.WARNING)
+
REQUIREMENTS = ['huawei-lte-api==1.0.16']
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
diff --git a/homeassistant/components/hue/.translations/pt.json b/homeassistant/components/hue/.translations/pt.json
index f7988d82d8c..d52540b0921 100644
--- a/homeassistant/components/hue/.translations/pt.json
+++ b/homeassistant/components/hue/.translations/pt.json
@@ -24,6 +24,6 @@
"title": "Link Hub"
}
},
- "title": ""
+ "title": "Philips Hue"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/hue/.translations/ro.json b/homeassistant/components/hue/.translations/ro.json
index 69cee1198d3..a2ecf8964b6 100644
--- a/homeassistant/components/hue/.translations/ro.json
+++ b/homeassistant/components/hue/.translations/ro.json
@@ -2,6 +2,8 @@
"config": {
"abort": {
"all_configured": "Toate pun\u021bile Philips Hue sunt deja configurate",
+ "already_configured": "Gateway-ul este deja configurat",
+ "cannot_connect": "Nu se poate conecta la gateway.",
"discover_timeout": "Imposibil de descoperit podurile Hue"
},
"error": {
diff --git a/homeassistant/components/ifttt/.translations/es.json b/homeassistant/components/ifttt/.translations/es.json
new file mode 100644
index 00000000000..13240ccefb1
--- /dev/null
+++ b/homeassistant/components/ifttt/.translations/es.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "description": "\u00bfEst\u00e1s seguro de que quieres configurar IFTTT?",
+ "title": "Configurar el applet de webhook IFTTT"
+ }
+ },
+ "title": "IFTTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ifttt/.translations/hu.json b/homeassistant/components/ifttt/.translations/hu.json
index a131f848d45..6ecf654ff47 100644
--- a/homeassistant/components/ifttt/.translations/hu.json
+++ b/homeassistant/components/ifttt/.translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "not_internet_accessible": "A Home Assistant-nek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l az IFTTT \u00fczenetek fogad\u00e1s\u00e1hoz."
+ },
"step": {
"user": {
"description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az IFTTT-t?",
diff --git a/homeassistant/components/ifttt/.translations/ko.json b/homeassistant/components/ifttt/.translations/ko.json
index 57ad8037753..2f033e4f4ee 100644
--- a/homeassistant/components/ifttt/.translations/ko.json
+++ b/homeassistant/components/ifttt/.translations/ko.json
@@ -5,7 +5,7 @@
"one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4."
},
"create_entry": {
- "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT Webhook \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n \ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url})\ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694."
+ "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT Webhook \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n \ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694."
},
"step": {
"user": {
diff --git a/homeassistant/components/ifttt/.translations/pl.json b/homeassistant/components/ifttt/.translations/pl.json
index 3c3c2182503..7064364ebe6 100644
--- a/homeassistant/components/ifttt/.translations/pl.json
+++ b/homeassistant/components/ifttt/.translations/pl.json
@@ -5,12 +5,12 @@
"one_instance_allowed": "Wymagana jest tylko jedna instancja."
},
"create_entry": {
- "default": "Aby wys\u0142a\u0107 zdarzenia do Home Assistant'a, b\u0119dziesz musia\u0142 u\u017cy\u0107 akcji \"Make a web request\" z [IFTTT Webhook apletu]({applet_url}). \n\n Podaj nast\u0119puj\u0105ce informacje:\n\n - URL: `{webhook_url}`\n - Metoda: POST\n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane."
+ "default": "Aby wys\u0142a\u0107 zdarzenia do Home Assistant'a, b\u0119dziesz musia\u0142 u\u017cy\u0107 akcji \"Make a web request\" z [IFTTT Webhook apletu]({applet_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}`\n - Metoda: POST\n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane."
},
"step": {
"user": {
- "description": "Jeste\u015b pewny, \u017ce chcesz skonfigurowa\u0107 IFTTT?",
- "title": "Konfigurowanie apletu Webhook IFTTT"
+ "description": "Czy chcesz skonfigurowa\u0107 IFTTT?",
+ "title": "Konfiguracja apletu Webhook IFTTT"
}
},
"title": "IFTTT"
diff --git a/homeassistant/components/ifttt/.translations/pt.json b/homeassistant/components/ifttt/.translations/pt.json
new file mode 100644
index 00000000000..08b7aee6a08
--- /dev/null
+++ b/homeassistant/components/ifttt/.translations/pt.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "A sua inst\u00e2ncia Home Assistent precisa de ser acess\u00edvel a partir da internet para receber mensagens IFTTT.",
+ "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria."
+ },
+ "create_entry": {
+ "default": "Para enviar eventos para o Home Assistente, precisa de utilizar a a\u00e7\u00e3o \"Make a web request\" no [IFTTT Webhook applet]({applet_url}).\n\nPreencha com a seguinte informa\u00e7\u00e3o:\n\n- URL: `{webhook_url}`\n- Method: POST \n- Content Type: application/json \n\nConsulte [a documenta\u00e7\u00e3o]({docs_url}) sobre como configurar automa\u00e7\u00f5es para lidar com dados de entrada."
+ },
+ "step": {
+ "user": {
+ "description": "Tem certeza de que deseja configurar o IFTTT?",
+ "title": "Configurar o IFTTT Webhook Applet"
+ }
+ },
+ "title": "IFTTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ifttt/.translations/ro.json b/homeassistant/components/ifttt/.translations/ro.json
index 03c77426671..dd7ae5f72cb 100644
--- a/homeassistant/components/ifttt/.translations/ro.json
+++ b/homeassistant/components/ifttt/.translations/ro.json
@@ -1,5 +1,9 @@
{
"config": {
+ "abort": {
+ "not_internet_accessible": "Instan\u021ba Home Assistant trebuie s\u0103 fie accesibil\u0103 de pe internet pentru a primi mesaje IFTTT.",
+ "one_instance_allowed": "Este necesar\u0103 o singur\u0103 instan\u021b\u0103."
+ },
"step": {
"user": {
"description": "Sigur dori\u021bi s\u0103 configura\u021bi IFTTT?"
diff --git a/homeassistant/components/ifttt/.translations/tr.json b/homeassistant/components/ifttt/.translations/tr.json
new file mode 100644
index 00000000000..80188b637f9
--- /dev/null
+++ b/homeassistant/components/ifttt/.translations/tr.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "IFTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py
index 60748d6ff13..76f01ad0aca 100644
--- a/homeassistant/components/ifttt/__init__.py
+++ b/homeassistant/components/ifttt/__init__.py
@@ -14,6 +14,7 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant import config_entries
+from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.util.network import is_local
REQUIREMENTS = ['pyfttt==0.3']
@@ -29,7 +30,6 @@ ATTR_VALUE2 = 'value2'
ATTR_VALUE3 = 'value3'
CONF_KEY = 'key'
-CONF_WEBHOOK_ID = 'webhook_id'
DOMAIN = 'ifttt'
@@ -91,13 +91,13 @@ async def handle_webhook(hass, webhook_id, request):
async def async_setup_entry(hass, entry):
"""Configure based on config entry."""
hass.components.webhook.async_register(
- entry.data['webhook_id'], handle_webhook)
+ entry.data[CONF_WEBHOOK_ID], handle_webhook)
return True
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
- hass.components.webhook.async_unregister(entry.data['webhook_id'])
+ hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
return True
diff --git a/homeassistant/components/image_processing/dlib_face_detect.py b/homeassistant/components/image_processing/dlib_face_detect.py
index d3b4e14f4de..cb9ea5ff5f9 100644
--- a/homeassistant/components/image_processing/dlib_face_detect.py
+++ b/homeassistant/components/image_processing/dlib_face_detect.py
@@ -58,8 +58,7 @@ class DlibFaceDetectEntity(ImageProcessingFaceEntity):
def process_image(self, image):
"""Process image."""
- # pylint: disable=import-error
- import face_recognition
+ import face_recognition # pylint: disable=import-error
fak_file = io.BytesIO(image)
fak_file.name = 'snapshot.jpg'
diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py
index 1e5d38b638e..062c18bb730 100644
--- a/homeassistant/components/image_processing/opencv.py
+++ b/homeassistant/components/image_processing/opencv.py
@@ -16,7 +16,7 @@ from homeassistant.components.image_processing import (
from homeassistant.core import split_entity_id
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['numpy==1.15.1']
+REQUIREMENTS = ['numpy==1.15.2']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py
index 924baeaa560..3980503a1ac 100644
--- a/homeassistant/components/insteon/__init__.py
+++ b/homeassistant/components/insteon/__init__.py
@@ -19,7 +19,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['insteonplm==0.14.2']
+REQUIREMENTS = ['insteonplm==0.15.0']
_LOGGER = logging.getLogger(__name__)
@@ -465,6 +465,16 @@ class InsteonEntity(Entity):
"""Return the INSTEON group that the entity responds to."""
return self._insteon_device_state.group
+ @property
+ def unique_id(self) -> str:
+ """Return a unique ID."""
+ if self._insteon_device_state.group == 0x01:
+ uid = self._insteon_device.id
+ else:
+ uid = '{:s}_{:d}'.format(self._insteon_device.id,
+ self._insteon_device_state.group)
+ return uid
+
@property
def name(self):
"""Return the name of the node (used for Entity_ID)."""
diff --git a/homeassistant/components/ios/.translations/pt.json b/homeassistant/components/ios/.translations/pt.json
index 6752606d9f5..d38b9abb70b 100644
--- a/homeassistant/components/ios/.translations/pt.json
+++ b/homeassistant/components/ios/.translations/pt.json
@@ -6,9 +6,9 @@
"step": {
"confirm": {
"description": "Deseja configurar o componente iOS do Home Assistant?",
- "title": ""
+ "title": "Home Assistant iOS"
}
},
- "title": ""
+ "title": "Home Assistant iOS"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ios/.translations/ro.json b/homeassistant/components/ios/.translations/ro.json
new file mode 100644
index 00000000000..5a83b5cd732
--- /dev/null
+++ b/homeassistant/components/ios/.translations/ro.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Este necesar\u0103 numai o singur\u0103 configurare a aplica\u021biei Home Assistant iOS."
+ },
+ "step": {
+ "confirm": {
+ "description": "Dori\u021bi s\u0103 configura\u021bi componenta Home Assistant iOS?",
+ "title": "Home Assistant iOS"
+ }
+ },
+ "title": "Home Assistant iOS"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py
index 58bb1fa5f42..16253ba271a 100644
--- a/homeassistant/components/keyboard.py
+++ b/homeassistant/components/keyboard.py
@@ -20,8 +20,7 @@ TAP_KEY_SCHEMA = vol.Schema({})
def setup(hass, config):
"""Listen for keyboard events."""
- # pylint: disable=import-error
- import pykeyboard
+ import pykeyboard # pylint: disable=import-error
keyboard = pykeyboard.PyKeyboard()
keyboard.special_key_assignment()
diff --git a/homeassistant/components/keyboard_remote.py b/homeassistant/components/keyboard_remote.py
index 9a7cc7caecb..ffc92f1949a 100644
--- a/homeassistant/components/keyboard_remote.py
+++ b/homeassistant/components/keyboard_remote.py
@@ -4,7 +4,6 @@ Receive signals from a keyboard and use it as a remote control.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/keyboard_remote/
"""
-# pylint: disable=import-error
import threading
import logging
import os
@@ -90,6 +89,7 @@ class KeyboardRemoteThread(threading.Thread):
id_folder = '/dev/input/by-id/'
if os.path.isdir(id_folder):
+ # pylint: disable=import-error
from evdev import InputDevice, list_devices
device_names = [InputDevice(file_name).name
for file_name in list_devices()]
@@ -104,6 +104,7 @@ class KeyboardRemoteThread(threading.Thread):
def _get_keyboard_device(self):
"""Get the keyboard device."""
+ # pylint: disable=import-error
from evdev import InputDevice, list_devices
if self.device_name:
devices = [InputDevice(file_name) for file_name in list_devices()]
@@ -121,6 +122,7 @@ class KeyboardRemoteThread(threading.Thread):
def run(self):
"""Run the loop of the KeyboardRemote."""
+ # pylint: disable=import-error
from evdev import categorize, ecodes
if self.dev is not None:
diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py
index b15963fa6bd..6aef3ea4ec5 100644
--- a/homeassistant/components/knx.py
+++ b/homeassistant/components/knx.py
@@ -240,7 +240,7 @@ class KNXModule:
async def telegram_received_cb(self, telegram):
"""Call invoked after a KNX telegram was received."""
- self.hass.bus.fire('knx_event', {
+ self.hass.bus.async_fire('knx_event', {
'address': str(telegram.group_address),
'data': telegram.payload.value
})
diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py
index 388fa41f36f..21e2fbba4c7 100644
--- a/homeassistant/components/konnected.py
+++ b/homeassistant/components/konnected.py
@@ -4,9 +4,11 @@ Support for Konnected devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/konnected/
"""
-import logging
+import asyncio
import hmac
import json
+import logging
+
import voluptuous as vol
from aiohttp.hdrs import AUTHORIZATION
@@ -16,17 +18,18 @@ from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
from homeassistant.components.discovery import SERVICE_KONNECTED
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
- HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED,
- CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES, CONF_HOST, CONF_PORT,
- CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, CONF_ACCESS_TOKEN,
- ATTR_ENTITY_ID, ATTR_STATE, STATE_ON)
-from homeassistant.helpers.dispatcher import async_dispatcher_send
+ EVENT_HOMEASSISTANT_START, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
+ HTTP_UNAUTHORIZED, CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES,
+ CONF_HOST, CONF_PORT, CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE,
+ CONF_ACCESS_TOKEN, ATTR_ENTITY_ID, ATTR_STATE, STATE_ON)
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_send, dispatcher_send)
from homeassistant.helpers import discovery
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['konnected==0.1.2']
+REQUIREMENTS = ['konnected==0.1.4']
DOMAIN = 'konnected'
@@ -36,6 +39,8 @@ CONF_MOMENTARY = 'momentary'
CONF_PAUSE = 'pause'
CONF_REPEAT = 'repeat'
CONF_INVERSE = 'inverse'
+CONF_BLINK = 'blink'
+CONF_DISCOVERY = 'discovery'
STATE_LOW = 'low'
STATE_HIGH = 'high'
@@ -49,7 +54,7 @@ _BINARY_SENSOR_SCHEMA = vol.All(
vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN),
vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_NAME): cv.string,
- vol.Optional(CONF_INVERSE): cv.boolean,
+ vol.Optional(CONF_INVERSE, default=False): cv.boolean,
}), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE)
)
@@ -81,6 +86,10 @@ CONFIG_SCHEMA = vol.Schema(
cv.ensure_list, [_BINARY_SENSOR_SCHEMA]),
vol.Optional(CONF_SWITCHES): vol.All(
cv.ensure_list, [_SWITCH_SCHEMA]),
+ vol.Optional(CONF_HOST): cv.string,
+ vol.Optional(CONF_PORT): cv.port,
+ vol.Optional(CONF_BLINK, default=True): cv.boolean,
+ vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
}],
}),
},
@@ -96,6 +105,8 @@ SIGNAL_SENSOR_UPDATE = 'konnected.{}.update'
async def async_setup(hass, config):
"""Set up the Konnected platform."""
+ import konnected
+
cfg = config.get(DOMAIN)
if cfg is None:
cfg = {}
@@ -107,10 +118,8 @@ async def async_setup(hass, config):
CONF_API_HOST: cfg.get(CONF_API_HOST)
}
- def device_discovered(service, info):
- """Call when a Konnected device has been discovered."""
- host = info.get(CONF_HOST)
- port = info.get(CONF_PORT)
+ def setup_device(host, port):
+ """Set up a Konnected device at `host` listening on `port`."""
discovered = DiscoveredDevice(hass, host, port)
if discovered.is_configured:
discovered.setup()
@@ -119,6 +128,33 @@ async def async_setup(hass, config):
" but not specified in configuration.yaml",
discovered.device_id)
+ def device_discovered(service, info):
+ """Call when a Konnected device has been discovered."""
+ host = info.get(CONF_HOST)
+ port = info.get(CONF_PORT)
+ setup_device(host, port)
+
+ async def manual_discovery(event):
+ """Init devices on the network with manually assigned addresses."""
+ specified = [dev for dev in cfg.get(CONF_DEVICES) if
+ dev.get(CONF_HOST) and dev.get(CONF_PORT)]
+
+ while specified:
+ for dev in specified:
+ _LOGGER.debug("Discovering Konnected device %s at %s:%s",
+ dev.get(CONF_ID),
+ dev.get(CONF_HOST),
+ dev.get(CONF_PORT))
+ try:
+ await hass.async_add_executor_job(setup_device,
+ dev.get(CONF_HOST),
+ dev.get(CONF_PORT))
+ specified.remove(dev)
+ except konnected.Client.ClientError as err:
+ _LOGGER.error(err)
+ await asyncio.sleep(10) # try again in 10 seconds
+
+ # Initialize devices specified in the configuration on boot
for device in cfg.get(CONF_DEVICES):
ConfiguredDevice(hass, device).save_data()
@@ -128,6 +164,7 @@ async def async_setup(hass, config):
device_discovered)
hass.http.register_view(KonnectedView(access_token))
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, manual_discovery)
return True
@@ -188,6 +225,8 @@ class ConfiguredDevice:
device_data = {
CONF_BINARY_SENSORS: sensors,
CONF_SWITCHES: actuators,
+ CONF_BLINK: self.config.get(CONF_BLINK),
+ CONF_DISCOVERY: self.config.get(CONF_DISCOVERY)
}
if CONF_DEVICES not in self.hass.data[DOMAIN]:
@@ -271,7 +310,7 @@ class DiscoveredDevice:
if sensor_config.get(CONF_INVERSE):
state = not state
- async_dispatcher_send(
+ dispatcher_send(
self.hass,
SIGNAL_SENSOR_UPDATE.format(entity_id),
state)
@@ -306,13 +345,19 @@ class DiscoveredDevice:
if (desired_sensor_configuration != current_sensor_configuration) or \
(current_actuator_config != desired_actuator_config) or \
- (current_api_endpoint != desired_api_endpoint):
+ (current_api_endpoint != desired_api_endpoint) or \
+ (self.status.get(CONF_BLINK) !=
+ self.stored_configuration.get(CONF_BLINK)) or \
+ (self.status.get(CONF_DISCOVERY) !=
+ self.stored_configuration.get(CONF_DISCOVERY)):
_LOGGER.info('pushing settings to device %s', self.device_id)
self.client.put_settings(
desired_sensor_configuration,
desired_actuator_config,
self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN),
- desired_api_endpoint
+ desired_api_endpoint,
+ blink=self.stored_configuration.get(CONF_BLINK),
+ discovery=self.stored_configuration.get(CONF_DISCOVERY)
)
diff --git a/homeassistant/components/lifx/.translations/es.json b/homeassistant/components/lifx/.translations/es.json
new file mode 100644
index 00000000000..f897c673432
--- /dev/null
+++ b/homeassistant/components/lifx/.translations/es.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "No se encontraron dispositivos LIFX en la red.",
+ "single_instance_allowed": "S\u00f3lo es posible una \u00fanica configuraci\u00f3n de LIFX."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u00bfQuieres configurar LIFX?",
+ "title": "LIFX"
+ }
+ },
+ "title": "LIFX"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lifx/.translations/pt.json b/homeassistant/components/lifx/.translations/pt.json
new file mode 100644
index 00000000000..d5c93c33993
--- /dev/null
+++ b/homeassistant/components/lifx/.translations/pt.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Nenhum dispositivo LIFX encontrado na rede.",
+ "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do LIFX \u00e9 permitida."
+ },
+ "step": {
+ "confirm": {
+ "description": "Deseja configurar o LIFX?",
+ "title": "LIFX"
+ }
+ },
+ "title": "LIFX"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py
new file mode 100644
index 00000000000..1ca6c00b23a
--- /dev/null
+++ b/homeassistant/components/lifx/__init__.py
@@ -0,0 +1,58 @@
+"""Component to embed LIFX."""
+import voluptuous as vol
+import homeassistant.helpers.config_validation as cv
+
+from homeassistant import config_entries
+from homeassistant.helpers import config_entry_flow
+from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
+
+
+DOMAIN = 'lifx'
+REQUIREMENTS = ['aiolifx==0.6.5']
+
+CONF_SERVER = 'server'
+CONF_BROADCAST = 'broadcast'
+
+INTERFACE_SCHEMA = vol.Schema({
+ vol.Optional(CONF_SERVER): cv.string,
+ vol.Optional(CONF_BROADCAST): cv.string,
+})
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: {
+ LIGHT_DOMAIN:
+ vol.Schema(vol.All(cv.ensure_list, [INTERFACE_SCHEMA])),
+ }
+}, extra=vol.ALLOW_EXTRA)
+
+
+async def async_setup(hass, config):
+ """Set up the LIFX component."""
+ conf = config.get(DOMAIN)
+
+ hass.data[DOMAIN] = conf or {}
+
+ if conf is not None:
+ hass.async_create_task(hass.config_entries.flow.async_init(
+ DOMAIN, context={'source': config_entries.SOURCE_IMPORT}))
+
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Set up LIFX from a config entry."""
+ hass.async_create_task(hass.config_entries.async_forward_entry_setup(
+ entry, LIGHT_DOMAIN))
+ return True
+
+
+async def _async_has_devices(hass):
+ """Return if there are devices that can be discovered."""
+ import aiolifx
+
+ lifx_ip_addresses = await aiolifx.LifxScan(hass.loop).scan()
+ return len(lifx_ip_addresses) > 0
+
+
+config_entry_flow.register_discovery_flow(
+ DOMAIN, 'LIFX', _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL)
diff --git a/homeassistant/components/lifx/strings.json b/homeassistant/components/lifx/strings.json
new file mode 100644
index 00000000000..300c9b628f3
--- /dev/null
+++ b/homeassistant/components/lifx/strings.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "title": "LIFX",
+ "step": {
+ "confirm": {
+ "title": "LIFX",
+ "description": "Do you want to set up LIFX?"
+ }
+ },
+ "abort": {
+ "single_instance_allowed": "Only a single configuration of LIFX is possible.",
+ "no_devices_found": "No LIFX devices found on the network."
+ }
+ }
+}
diff --git a/homeassistant/components/light/abode.py b/homeassistant/components/light/abode.py
index 69314b63a4b..397d61f3073 100644
--- a/homeassistant/components/light/abode.py
+++ b/homeassistant/components/light/abode.py
@@ -8,9 +8,10 @@ import logging
from math import ceil
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
from homeassistant.components.light import (
- ATTR_BRIGHTNESS, ATTR_HS_COLOR,
- SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light)
-import homeassistant.util.color as color_util
+ ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_COLOR_TEMP,
+ SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light)
+from homeassistant.util.color import (
+ color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin)
DEPENDENCIES = ['abode']
@@ -45,10 +46,13 @@ class AbodeLight(AbodeDevice, Light):
def turn_on(self, **kwargs):
"""Turn on the light."""
- if (ATTR_HS_COLOR in kwargs and
- self._device.is_dimmable and self._device.has_color):
- self._device.set_color(color_util.color_hs_to_RGB(
- *kwargs[ATTR_HS_COLOR]))
+ if ATTR_COLOR_TEMP in kwargs and self._device.is_color_capable:
+ self._device.set_color_temp(
+ int(color_temperature_mired_to_kelvin(
+ kwargs[ATTR_COLOR_TEMP])))
+
+ if ATTR_HS_COLOR in kwargs and self._device.is_color_capable:
+ self._device.set_color(kwargs[ATTR_HS_COLOR])
if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable:
# Convert HASS brightness (0-255) to Abode brightness (0-99)
@@ -77,18 +81,23 @@ class AbodeLight(AbodeDevice, Light):
# Convert Abode brightness (0-99) to HASS brightness (0-255)
return ceil(brightness * 255 / 99.0)
+ @property
+ def color_temp(self):
+ """Return the color temp of the light."""
+ if self._device.has_color:
+ return color_temperature_kelvin_to_mired(self._device.color_temp)
+
@property
def hs_color(self):
"""Return the color of the light."""
- if self._device.is_dimmable and self._device.has_color:
- return color_util.color_RGB_to_hs(*self._device.color)
+ if self._device.has_color:
+ return self._device.color
@property
def supported_features(self):
"""Flag supported features."""
- if self._device.is_dimmable and self._device.has_color:
- return SUPPORT_BRIGHTNESS | SUPPORT_COLOR
+ if self._device.is_dimmable and self._device.is_color_capable:
+ return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP
if self._device.is_dimmable:
return SUPPORT_BRIGHTNESS
-
return 0
diff --git a/homeassistant/components/light/elkm1.py b/homeassistant/components/light/elkm1.py
new file mode 100644
index 00000000000..707aedbb161
--- /dev/null
+++ b/homeassistant/components/light/elkm1.py
@@ -0,0 +1,59 @@
+"""
+Support for control of ElkM1 lighting (X10, UPB, etc).
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/light.elkm1/
+"""
+
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
+from homeassistant.components.elkm1 import (
+ DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities)
+
+DEPENDENCIES = [ELK_DOMAIN]
+
+
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Set up the Elk light platform."""
+ if discovery_info is None:
+ return
+ elk = hass.data[ELK_DOMAIN]['elk']
+ async_add_entities(
+ create_elk_entities(hass, elk.lights, 'plc', ElkLight, []), True)
+
+
+class ElkLight(ElkEntity, Light):
+ """Elk lighting device."""
+
+ def __init__(self, element, elk, elk_data):
+ """Initialize light."""
+ super().__init__(element, elk, elk_data)
+ self._brightness = self._element.status
+
+ @property
+ def brightness(self):
+ """Get the brightness."""
+ return self._brightness
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return SUPPORT_BRIGHTNESS
+
+ @property
+ def is_on(self) -> bool:
+ """Get the current brightness."""
+ return self._brightness != 0
+
+ def _element_changed(self, element, changeset):
+ status = self._element.status if self._element.status != 1 else 100
+ self._brightness = round(status * 2.55)
+
+ async def async_turn_on(self, **kwargs):
+ """Turn on the light."""
+ self._element.level(round(kwargs.get(ATTR_BRIGHTNESS, 255) / 2.55))
+
+ async def async_turn_off(self, **kwargs):
+ """Turn off the light."""
+ self._element.level(0)
diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py
index f389d34cd5d..cab6957c265 100644
--- a/homeassistant/components/light/flux_led.py
+++ b/homeassistant/components/light/flux_led.py
@@ -18,7 +18,7 @@ from homeassistant.components.light import (
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
-REQUIREMENTS = ['flux_led==0.21']
+REQUIREMENTS = ['flux_led==0.22']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/light/homekit_controller.py b/homeassistant/components/light/homekit_controller.py
index dd24e3cfb2e..b874bc49f0e 100644
--- a/homeassistant/components/light/homekit_controller.py
+++ b/homeassistant/components/light/homekit_controller.py
@@ -38,8 +38,7 @@ class HomeKitLight(HomeKitEntity, Light):
def update_characteristics(self, characteristics):
"""Synchronise light state with Home Assistant."""
- # pylint: disable=import-error
- import homekit
+ import homekit # pylint: disable=import-error
for characteristic in characteristics:
ctype = characteristic['type']
diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py
index 778d2fac59c..a1423cc6682 100644
--- a/homeassistant/components/light/knx.py
+++ b/homeassistant/components/light/knx.py
@@ -52,7 +52,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(KNXLight(hass, device))
+ entities.append(KNXLight(device))
async_add_entities(entities)
@@ -71,17 +71,15 @@ def async_add_entities_config(hass, config, async_add_entities):
group_address_color=config.get(CONF_COLOR_ADDRESS),
group_address_color_state=config.get(CONF_COLOR_STATE_ADDRESS))
hass.data[DATA_KNX].xknx.devices.add(light)
- async_add_entities([KNXLight(hass, light)])
+ async_add_entities([KNXLight(light)])
class KNXLight(Light):
"""Representation of a KNX light."""
- def __init__(self, hass, device):
+ def __init__(self, device):
"""Initialize of KNX light."""
self.device = device
- self.hass = hass
- self.async_register_callbacks()
@callback
def async_register_callbacks(self):
@@ -91,6 +89,10 @@ class KNXLight(Light):
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."""
diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py
index 9dcd2ae4cc2..f346f88c42b 100644
--- a/homeassistant/components/light/lifx.py
+++ b/homeassistant/components/light/lifx.py
@@ -17,36 +17,32 @@ from homeassistant import util
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_COLOR_TEMP,
ATTR_EFFECT, ATTR_HS_COLOR, ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION,
- ATTR_XY_COLOR, COLOR_GROUP, DOMAIN, LIGHT_TURN_ON_SCHEMA, PLATFORM_SCHEMA,
+ ATTR_XY_COLOR, COLOR_GROUP, DOMAIN, LIGHT_TURN_ON_SCHEMA,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT,
SUPPORT_TRANSITION, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, Light,
preprocess_turn_on_alternatives)
+from homeassistant.components.lifx import (
+ DOMAIN as LIFX_DOMAIN, CONF_SERVER, CONF_BROADCAST)
from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
+import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.service import extract_entity_ids
import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['aiolifx==0.6.3', 'aiolifx_effects==0.2.1']
+DEPENDENCIES = ['lifx']
+REQUIREMENTS = ['aiolifx_effects==0.2.1']
-UDP_BROADCAST_PORT = 56700
+SCAN_INTERVAL = timedelta(seconds=10)
DISCOVERY_INTERVAL = 60
MESSAGE_TIMEOUT = 1.0
MESSAGE_RETRIES = 8
UNAVAILABLE_GRACE = 90
-CONF_SERVER = 'server'
-CONF_BROADCAST = 'broadcast'
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
- vol.Optional(CONF_BROADCAST, default='255.255.255.255'): cv.string,
-})
-
SERVICE_LIFX_SET_STATE = 'lifx_set_state'
ATTR_INFRARED = 'infrared'
@@ -138,24 +134,41 @@ async def async_setup_platform(hass,
config,
async_add_entities,
discovery_info=None):
- """Set up the LIFX platform."""
+ """Set up the LIFX light platform. Obsolete."""
+ _LOGGER.warning('LIFX no longer works with light platform configuration.')
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up LIFX from a config entry."""
if sys.platform == 'win32':
_LOGGER.warning("The lifx platform is known to not work on Windows. "
"Consider using the lifx_legacy platform instead")
- server_addr = config.get(CONF_SERVER)
+ # Priority 1: manual config
+ interfaces = hass.data[LIFX_DOMAIN].get(DOMAIN)
+ if not interfaces:
+ # Priority 2: scanned interfaces
+ lifx_ip_addresses = await aiolifx().LifxScan(hass.loop).scan()
+ interfaces = [{CONF_SERVER: ip} for ip in lifx_ip_addresses]
+ if not interfaces:
+ # Priority 3: default interface
+ interfaces = [{}]
lifx_manager = LIFXManager(hass, async_add_entities)
- lifx_discovery = aiolifx().LifxDiscovery(
- hass.loop,
- lifx_manager,
- discovery_interval=DISCOVERY_INTERVAL,
- broadcast_ip=config.get(CONF_BROADCAST))
- coro = hass.loop.create_datagram_endpoint(
- lambda: lifx_discovery, local_addr=(server_addr, UDP_BROADCAST_PORT))
+ for interface in interfaces:
+ kwargs = {'discovery_interval': DISCOVERY_INTERVAL}
+ broadcast_ip = interface.get(CONF_BROADCAST)
+ if broadcast_ip:
+ kwargs['broadcast_ip'] = broadcast_ip
+ lifx_discovery = aiolifx().LifxDiscovery(
+ hass.loop, lifx_manager, **kwargs)
- hass.async_add_job(coro)
+ kwargs = {}
+ listen_ip = interface.get(CONF_SERVER)
+ if listen_ip:
+ kwargs['listen_ip'] = listen_ip
+ lifx_discovery.start(**kwargs)
@callback
def cleanup(event):
@@ -225,7 +238,7 @@ class LIFXManager:
for light in self.service_to_entities(service):
if service.service == SERVICE_LIFX_SET_STATE:
task = light.set_state(**service.data)
- tasks.append(self.hass.async_add_job(task))
+ tasks.append(self.hass.async_create_task(task))
if tasks:
await asyncio.wait(tasks, loop=self.hass.loop)
@@ -392,6 +405,26 @@ class LIFXLight(Light):
self.postponed_update = None
self.lock = asyncio.Lock()
+ @property
+ def device_info(self):
+ """Return information about the device."""
+ info = {
+ 'identifiers': {
+ (LIFX_DOMAIN, self.unique_id)
+ },
+ 'name': self.name,
+ 'connections': {
+ (dr.CONNECTION_NETWORK_MAC, self.bulb.mac_addr)
+ },
+ 'manufacturer': 'LIFX',
+ }
+
+ model = aiolifx().products.product_map.get(self.bulb.product)
+ if model is not None:
+ info['model'] = model
+
+ return info
+
@property
def available(self):
"""Return the availability of the bulb."""
diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py
index a5aeabba84d..2e2971cfdc2 100644
--- a/homeassistant/components/light/limitlessled.py
+++ b/homeassistant/components/light/limitlessled.py
@@ -20,7 +20,7 @@ from homeassistant.util.color import (
color_temperature_mired_to_kelvin, color_hs_to_RGB)
from homeassistant.helpers.restore_state import async_get_last_state
-REQUIREMENTS = ['limitlessled==1.1.2']
+REQUIREMENTS = ['limitlessled==1.1.3']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py
index 3b095aa4bfd..92030c8617a 100644
--- a/homeassistant/components/light/mqtt.py
+++ b/homeassistant/components/light/mqtt.py
@@ -15,7 +15,7 @@ from homeassistant.components.light import (
ATTR_WHITE_VALUE, Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE)
from homeassistant.const import (
- CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME,
+ CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_HS, CONF_NAME,
CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, STATE_ON,
CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY)
from homeassistant.components.mqtt import (
@@ -44,6 +44,9 @@ CONF_EFFECT_COMMAND_TOPIC = 'effect_command_topic'
CONF_EFFECT_LIST = 'effect_list'
CONF_EFFECT_STATE_TOPIC = 'effect_state_topic'
CONF_EFFECT_VALUE_TEMPLATE = 'effect_value_template'
+CONF_HS_COMMAND_TOPIC = 'hs_command_topic'
+CONF_HS_STATE_TOPIC = 'hs_state_topic'
+CONF_HS_VALUE_TEMPLATE = 'hs_value_template'
CONF_RGB_COMMAND_TEMPLATE = 'rgb_command_template'
CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic'
CONF_RGB_STATE_TOPIC = 'rgb_state_topic'
@@ -82,6 +85,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_EFFECT_VALUE_TEMPLATE): cv.template,
+ vol.Optional(CONF_HS_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(CONF_HS_STATE_TOPIC): mqtt.valid_subscribe_topic,
+ vol.Optional(CONF_HS_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
@@ -143,6 +149,8 @@ async def _async_setup_entity(hass, config, async_add_entities,
CONF_COMMAND_TOPIC,
CONF_EFFECT_COMMAND_TOPIC,
CONF_EFFECT_STATE_TOPIC,
+ CONF_HS_COMMAND_TOPIC,
+ CONF_HS_STATE_TOPIC,
CONF_RGB_COMMAND_TOPIC,
CONF_RGB_STATE_TOPIC,
CONF_STATE_TOPIC,
@@ -156,6 +164,7 @@ async def _async_setup_entity(hass, config, async_add_entities,
CONF_BRIGHTNESS: config.get(CONF_BRIGHTNESS_VALUE_TEMPLATE),
CONF_COLOR_TEMP: config.get(CONF_COLOR_TEMP_VALUE_TEMPLATE),
CONF_EFFECT: config.get(CONF_EFFECT_VALUE_TEMPLATE),
+ CONF_HS: config.get(CONF_HS_VALUE_TEMPLATE),
CONF_RGB: config.get(CONF_RGB_VALUE_TEMPLATE),
CONF_RGB_COMMAND_TEMPLATE: config.get(CONF_RGB_COMMAND_TEMPLATE),
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
@@ -202,11 +211,17 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light):
self._optimistic_rgb = \
optimistic or topic[CONF_RGB_STATE_TOPIC] is None
self._optimistic_brightness = (
- optimistic or topic[CONF_BRIGHTNESS_STATE_TOPIC] is None)
+ optimistic or
+ (topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None and
+ topic[CONF_BRIGHTNESS_STATE_TOPIC] is None) or
+ (topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is None and
+ topic[CONF_RGB_STATE_TOPIC] is None))
self._optimistic_color_temp = (
optimistic or topic[CONF_COLOR_TEMP_STATE_TOPIC] is None)
self._optimistic_effect = (
optimistic or topic[CONF_EFFECT_STATE_TOPIC] is None)
+ self._optimistic_hs = \
+ optimistic or topic[CONF_HS_STATE_TOPIC] is None
self._optimistic_white_value = (
optimistic or topic[CONF_WHITE_VALUE_STATE_TOPIC] is None)
self._optimistic_xy = \
@@ -222,7 +237,8 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light):
self._white_value = None
self._supported_features = 0
self._supported_features |= (
- topic[CONF_RGB_COMMAND_TOPIC] is not None and SUPPORT_COLOR)
+ topic[CONF_RGB_COMMAND_TOPIC] is not None and
+ (SUPPORT_COLOR | SUPPORT_BRIGHTNESS))
self._supported_features |= (
topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None and
SUPPORT_BRIGHTNESS)
@@ -232,6 +248,8 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light):
self._supported_features |= (
topic[CONF_EFFECT_COMMAND_TOPIC] is not None and
SUPPORT_EFFECT)
+ self._supported_features |= (
+ topic[CONF_HS_COMMAND_TOPIC] is not None and SUPPORT_COLOR)
self._supported_features |= (
topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None and
SUPPORT_WHITE_VALUE)
@@ -312,6 +330,10 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light):
rgb = [int(val) for val in payload.split(',')]
self._hs = color_util.color_RGB_to_hs(*rgb)
+ if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is None:
+ percent_bright = \
+ float(color_util.color_RGB_to_hsv(*rgb)[2]) / 100.0
+ self._brightness = int(percent_bright * 255)
self.async_schedule_update_ha_state()
if self._topic[CONF_RGB_STATE_TOPIC] is not None:
@@ -374,6 +396,33 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light):
else:
self._effect = None
+ @callback
+ def hs_received(topic, payload, qos):
+ """Handle new MQTT messages for hs color."""
+ payload = templates[CONF_HS](payload)
+ if not payload:
+ _LOGGER.debug("Ignoring empty hs message from '%s'", topic)
+ return
+
+ try:
+ hs_color = [float(val) for val in payload.split(',', 2)]
+ self._hs = hs_color
+ self.async_schedule_update_ha_state()
+ except ValueError:
+ _LOGGER.debug("Failed to parse hs state update: '%s'",
+ payload)
+
+ if self._topic[CONF_HS_STATE_TOPIC] is not None:
+ await mqtt.async_subscribe(
+ self.hass, self._topic[CONF_HS_STATE_TOPIC], hs_received,
+ self._qos)
+ self._hs = (0, 0)
+ if self._optimistic_hs and last_state\
+ and last_state.attributes.get(ATTR_HS_COLOR):
+ self._hs = last_state.attributes.get(ATTR_HS_COLOR)
+ elif self._topic[CONF_HS_COMMAND_TOPIC] is not None:
+ self._hs = (0, 0)
+
@callback
def white_value_received(topic, payload, qos):
"""Handle new MQTT messages for white value."""
@@ -403,7 +452,7 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light):
@callback
def xy_received(topic, payload, qos):
- """Handle new MQTT messages for color."""
+ """Handle new MQTT messages for xy color."""
payload = templates[CONF_XY](payload)
if not payload:
_LOGGER.debug("Ignoring empty xy-color message from '%s'",
@@ -539,6 +588,19 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light):
self._hs = kwargs[ATTR_HS_COLOR]
should_update = True
+ if ATTR_HS_COLOR in kwargs and \
+ self._topic[CONF_HS_COMMAND_TOPIC] is not None:
+
+ hs_color = kwargs[ATTR_HS_COLOR]
+ mqtt.async_publish(
+ self.hass, self._topic[CONF_HS_COMMAND_TOPIC],
+ '{},{}'.format(*hs_color), self._qos,
+ self._retain)
+
+ if self._optimistic_hs:
+ self._hs = kwargs[ATTR_HS_COLOR]
+ should_update = True
+
if ATTR_HS_COLOR in kwargs and \
self._topic[CONF_XY_COMMAND_TOPIC] is not None:
@@ -563,6 +625,27 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light):
if self._optimistic_brightness:
self._brightness = kwargs[ATTR_BRIGHTNESS]
should_update = True
+ elif ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR not in kwargs and\
+ self._topic[CONF_RGB_COMMAND_TOPIC] is not None:
+ rgb = color_util.color_hsv_to_RGB(
+ self._hs[0], self._hs[1], kwargs[ATTR_BRIGHTNESS] / 255 * 100)
+ tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE]
+ if tpl:
+ rgb_color_str = tpl.async_render({
+ 'red': rgb[0],
+ 'green': rgb[1],
+ 'blue': rgb[2],
+ })
+ else:
+ rgb_color_str = '{},{},{}'.format(*rgb)
+
+ mqtt.async_publish(
+ self.hass, self._topic[CONF_RGB_COMMAND_TOPIC],
+ rgb_color_str, self._qos, self._retain)
+
+ if self._optimistic_brightness:
+ self._brightness = kwargs[ATTR_BRIGHTNESS]
+ should_update = True
if ATTR_COLOR_TEMP in kwargs and \
self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None:
diff --git a/homeassistant/components/light/opple.py b/homeassistant/components/light/opple.py
index 66850d04406..fb503d33d31 100644
--- a/homeassistant/components/light/opple.py
+++ b/homeassistant/components/light/opple.py
@@ -110,7 +110,7 @@ class OppleLight(Light):
self._device.brightness = kwargs[ATTR_BRIGHTNESS]
if ATTR_COLOR_TEMP in kwargs and \
- self.brightness != kwargs[ATTR_COLOR_TEMP]:
+ self.color_temp != kwargs[ATTR_COLOR_TEMP]:
color_temp = mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])
self._device.color_temperature = color_temp
diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py
index 244a233c517..a49e12c76a6 100644
--- a/homeassistant/components/light/osramlightify.py
+++ b/homeassistant/components/light/osramlightify.py
@@ -231,6 +231,11 @@ class OsramLightifyLight(Luminary):
self._luminary.temp())
self._brightness = int(self._luminary.lum() * 2.55)
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._light_id
+
class OsramLightifyGroup(Luminary):
"""Representation of an Osram Lightify Group."""
@@ -240,6 +245,7 @@ class OsramLightifyGroup(Luminary):
self._bridge = bridge
self._light_ids = []
super().__init__(group, update_lights)
+ self._unique_id = '{}'.format(self._light_ids)
def _get_state(self):
"""Get state of group."""
@@ -260,3 +266,8 @@ class OsramLightifyGroup(Luminary):
else:
self._temperature = color_temperature_kelvin_to_mired(o_temp)
self._state = light.on()
+
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._unique_id
diff --git a/homeassistant/components/light/rflink.py b/homeassistant/components/light/rflink.py
index d9f9dd589ec..3b60280c582 100644
--- a/homeassistant/components/light/rflink.py
+++ b/homeassistant/components/light/rflink.py
@@ -6,19 +6,19 @@ https://home-assistant.io/components/light.rflink/
"""
import logging
+import voluptuous as vol
+
from homeassistant.components.light import (
- ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
+ ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light)
from homeassistant.components.rflink import (
CONF_ALIASES, CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICE_DEFAULTS,
CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES,
- CONF_GROUP_ALIASSES, CONF_IGNORE_DEVICES, CONF_NOGROUP_ALIASES,
- CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER,
- DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA,
- DOMAIN, EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv,
- remove_deprecated, vol)
-from homeassistant.const import (
- CONF_NAME, CONF_PLATFORM, CONF_TYPE, STATE_UNKNOWN)
-from homeassistant.helpers.deprecation import get_deprecated
+ CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES,
+ CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER, DEVICE_DEFAULTS_SCHEMA,
+ EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice,
+ remove_deprecated)
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import (CONF_NAME, CONF_TYPE)
DEPENDENCIES = ['rflink']
@@ -29,14 +29,12 @@ TYPE_SWITCHABLE = 'switchable'
TYPE_HYBRID = 'hybrid'
TYPE_TOGGLE = 'toggle'
-PLATFORM_SCHEMA = vol.Schema({
- vol.Required(CONF_PLATFORM): DOMAIN,
- vol.Optional(CONF_IGNORE_DEVICES): vol.All(cv.ensure_list, [cv.string]),
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})):
- DEVICE_DEFAULTS_SCHEMA,
+ DEVICE_DEFAULTS_SCHEMA,
vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean,
- vol.Optional(CONF_DEVICES, default={}): vol.Schema({
- cv.string: {
+ vol.Optional(CONF_DEVICES, default={}): {
+ cv.string: vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_TYPE):
vol.Any(TYPE_DIMMABLE, TYPE_SWITCHABLE,
@@ -57,9 +55,9 @@ PLATFORM_SCHEMA = vol.Schema({
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_NOGROUP_ALIASSES):
vol.All(cv.ensure_list, [cv.string]),
- },
- }),
-})
+ })
+ },
+}, extra=vol.ALLOW_EXTRA)
def entity_type_for_device_id(device_id):
@@ -98,7 +96,7 @@ def entity_class_for_type(entity_type):
return entity_device_mapping.get(entity_type, RflinkLight)
-def devices_from_config(domain_config, hass=None):
+def devices_from_config(domain_config):
"""Parse configuration and add Rflink light devices."""
devices = []
for device_id, config in domain_config[CONF_DEVICES].items():
@@ -124,40 +122,16 @@ def devices_from_config(domain_config, hass=None):
"repetitions. Please set 'dimmable' or 'switchable' "
"type explicitly in configuration", device_id)
- device = entity_class(device_id, hass, **device_config)
+ device = entity_class(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 get_deprecated(config, CONF_ALIASES, CONF_ALIASSES):
- hass.data[DATA_ENTITY_LOOKUP][
- EVENT_KEY_COMMAND][_id].append(device)
- hass.data[DATA_ENTITY_GROUP_LOOKUP][
- EVENT_KEY_COMMAND][_id].append(device)
- # group_aliases only respond to group commands
- for _id in get_deprecated(
- config, CONF_GROUP_ALIASES, CONF_GROUP_ALIASSES):
- hass.data[DATA_ENTITY_GROUP_LOOKUP][
- EVENT_KEY_COMMAND][_id].append(device)
- # nogroup_aliases only respond to normal commands
- for _id in get_deprecated(
- config, CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES):
- hass.data[DATA_ENTITY_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 light platform."""
- async_add_entities(devices_from_config(config, hass))
+ async_add_entities(devices_from_config(config))
async def add_new_device(event):
"""Check if device is known, otherwise add to list of known devices."""
@@ -167,16 +141,9 @@ async def async_setup_platform(hass, config, async_add_entities,
entity_class = entity_class_for_type(entity_type)
device_config = config[CONF_DEVICE_DEFAULTS]
- device = entity_class(device_id, hass, **device_config)
+ device = entity_class(device_id, initial_event=event, **device_config)
async_add_entities([device])
- # Register entity to listen to incoming Rflink events
- hass.data[DATA_ENTITY_LOOKUP][
- EVENT_KEY_COMMAND][device_id].append(device)
-
- # Schedule task to process event after entity is created
- hass.async_add_job(device.handle_event, event)
-
if config[CONF_AUTOMATIC_ADD]:
hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_COMMAND] = add_new_device
@@ -277,7 +244,7 @@ class ToggleRflinkLight(SwitchableRflinkDevice, Light):
if command == 'on':
# if the state is unknown or false, it gets set as true
# if the state is true, it gets set as false
- self._state = self._state in [STATE_UNKNOWN, False]
+ self._state = self._state in [None, False]
async def async_turn_on(self, **kwargs):
"""Turn the device on."""
diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py
index 8aff85c6001..2447dabe3c7 100644
--- a/homeassistant/components/light/template.py
+++ b/homeassistant/components/light/template.py
@@ -215,8 +215,8 @@ class LightTemplate(Light):
optimistic_set = True
if ATTR_BRIGHTNESS in kwargs and self._level_script:
- self.hass.async_create_task(self._level_script.async_run(
- {"brightness": kwargs[ATTR_BRIGHTNESS]}))
+ await self._level_script.async_run(
+ {"brightness": kwargs[ATTR_BRIGHTNESS]}, context=self._context)
else:
await self._on_script.async_run()
@@ -225,7 +225,7 @@ class LightTemplate(Light):
async def async_turn_off(self, **kwargs):
"""Turn the light off."""
- await self._off_script.async_run()
+ await self._off_script.async_run(context=self._context)
if self._template is None:
self._state = False
self.async_schedule_update_ha_state()
diff --git a/homeassistant/components/light/tuya.py b/homeassistant/components/light/tuya.py
index 0dc2cacc1e0..0a1468a6a51 100644
--- a/homeassistant/components/light/tuya.py
+++ b/homeassistant/components/light/tuya.py
@@ -40,17 +40,17 @@ class TuyaLight(TuyaDevice, Light):
@property
def brightness(self):
"""Return the brightness of the light."""
- return self.tuya.brightness()
+ return int(self.tuya.brightness())
@property
def hs_color(self):
"""Return the hs_color of the light."""
- return self.tuya.hs_color()
+ return tuple(map(int, self.tuya.hs_color()))
@property
def color_temp(self):
"""Return the color_temp of the light."""
- color_temp = self.tuya.color_temp()
+ color_temp = int(self.tuya.color_temp())
if color_temp is None:
return None
return colorutil.color_temperature_kelvin_to_mired(color_temp)
diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py
index 8bc2497a3e5..26a63b2f16b 100644
--- a/homeassistant/components/light/xiaomi_miio.py
+++ b/homeassistant/components/light/xiaomi_miio.py
@@ -5,23 +5,24 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/light.xiaomi_miio/
"""
import asyncio
+import datetime
+from datetime import timedelta
from functools import partial
import logging
from math import ceil
-from datetime import timedelta
-import datetime
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.light import (
- PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS,
- ATTR_COLOR_TEMP, SUPPORT_COLOR_TEMP, Light, ATTR_ENTITY_ID, DOMAIN, )
-
-from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, )
+ ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_ENTITY_ID, DOMAIN, PLATFORM_SCHEMA,
+ SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, Light)
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
from homeassistant.exceptions import PlatformNotReady
+import homeassistant.helpers.config_validation as cv
from homeassistant.util import dt
+REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45']
+
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Xiaomi Philips Light'
@@ -39,11 +40,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
'philips.light.zyceiling',
'philips.light.bulb',
'philips.light.candle',
- 'philips.light.candle2']),
+ 'philips.light.candle2',
+ 'philips.light.mono1']),
})
-REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41']
-
# The light does not accept cct values < 1
CCT_MIN = 1
CCT_MAX = 100
@@ -157,6 +157,12 @@ async def async_setup_platform(hass, config, async_add_entities,
device = XiaomiPhilipsBulb(name, light, model, unique_id)
devices.append(device)
hass.data[DATA_KEY][host] = device
+ elif model == 'philips.light.mono1':
+ from miio import PhilipsBulb
+ light = PhilipsBulb(host, token)
+ device = XiaomiPhilipsGenericLight(name, light, model, unique_id)
+ devices.append(device)
+ hass.data[DATA_KEY][host] = device
else:
_LOGGER.error(
'Unsupported device found! Please create an issue at '
diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py
index b14b1f96e69..7efd62e3de5 100644
--- a/homeassistant/components/light/yeelight.py
+++ b/homeassistant/components/light/yeelight.py
@@ -20,7 +20,7 @@ from homeassistant.components.light import (
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
-REQUIREMENTS = ['yeelight==0.4.0']
+REQUIREMENTS = ['yeelight==0.4.3']
_LOGGER = logging.getLogger(__name__)
@@ -35,6 +35,7 @@ LEGACY_DEVICE_TYPE_MAP = {
DEFAULT_NAME = 'Yeelight'
DEFAULT_TRANSITION = 350
+CONF_MODEL = 'model'
CONF_TRANSITION = 'transition'
CONF_SAVE_ON_CHANGE = 'save_on_change'
CONF_MODE_MUSIC = 'use_music_mode'
@@ -46,6 +47,7 @@ DEVICE_SCHEMA = vol.Schema({
vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int,
vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean,
vol.Optional(CONF_SAVE_ON_CHANGE, default=False): cv.boolean,
+ vol.Optional(CONF_MODEL): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
@@ -55,15 +57,14 @@ SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS |
SUPPORT_TRANSITION |
SUPPORT_FLASH)
+SUPPORT_YEELIGHT_WHITE_TEMP = (SUPPORT_YEELIGHT |
+ SUPPORT_COLOR_TEMP)
+
SUPPORT_YEELIGHT_RGB = (SUPPORT_YEELIGHT |
SUPPORT_COLOR |
SUPPORT_EFFECT |
SUPPORT_COLOR_TEMP)
-YEELIGHT_MIN_KELVIN = YEELIGHT_MAX_KELVIN = 2700
-YEELIGHT_RGB_MIN_KELVIN = 1700
-YEELIGHT_RGB_MAX_KELVIN = 6500
-
EFFECT_DISCO = "Disco"
EFFECT_TEMP = "Slow Temp"
EFFECT_STROBE = "Strobe epilepsy!"
@@ -132,23 +133,26 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.debug("Adding autodetected %s", discovery_info['hostname'])
device_type = discovery_info['device_type']
- device_type = LEGACY_DEVICE_TYPE_MAP.get(device_type, device_type)
+ legacy_device_type = LEGACY_DEVICE_TYPE_MAP.get(device_type,
+ device_type)
# Not using hostname, as it seems to vary.
- name = "yeelight_%s_%s" % (device_type,
+ name = "yeelight_%s_%s" % (legacy_device_type,
discovery_info['properties']['mac'])
- host = discovery_info['host']
- device = {'name': name, 'ipaddr': host}
+ device = {'name': name, 'ipaddr': discovery_info['host']}
- light = YeelightLight(device, DEVICE_SCHEMA({}))
+ light = YeelightLight(device, DEVICE_SCHEMA({CONF_MODEL: device_type}))
lights.append(light)
- hass.data[DATA_KEY][host] = light
+ hass.data[DATA_KEY][name] = light
else:
- for host, device_config in config[CONF_DEVICES].items():
- device = {'name': device_config[CONF_NAME], 'ipaddr': host}
+ for ipaddr, device_config in config[CONF_DEVICES].items():
+ name = device_config[CONF_NAME]
+ _LOGGER.debug("Adding configured %s", name)
+
+ device = {'name': name, 'ipaddr': ipaddr}
light = YeelightLight(device, device_config)
lights.append(light)
- hass.data[DATA_KEY][host] = light
+ hass.data[DATA_KEY][name] = light
add_entities(lights, True)
@@ -194,6 +198,10 @@ class YeelightLight(Light):
self._is_on = None
self._hs = None
+ self._model = config.get('model')
+ self._min_mireds = None
+ self._max_mireds = None
+
@property
def available(self) -> bool:
"""Return if bulb is available."""
@@ -232,16 +240,12 @@ class YeelightLight(Light):
@property
def min_mireds(self):
"""Return minimum supported color temperature."""
- if self.supported_features & SUPPORT_COLOR_TEMP:
- return kelvin_to_mired(YEELIGHT_RGB_MAX_KELVIN)
- return kelvin_to_mired(YEELIGHT_MAX_KELVIN)
+ return self._min_mireds
@property
def max_mireds(self):
"""Return maximum supported color temperature."""
- if self.supported_features & SUPPORT_COLOR_TEMP:
- return kelvin_to_mired(YEELIGHT_RGB_MIN_KELVIN)
- return kelvin_to_mired(YEELIGHT_MIN_KELVIN)
+ return self._max_mireds
def _get_hs_from_properties(self):
rgb = self._properties.get('rgb', None)
@@ -279,7 +283,8 @@ class YeelightLight(Light):
import yeelight
if self._bulb_device is None:
try:
- self._bulb_device = yeelight.Bulb(self._ipaddr)
+ self._bulb_device = yeelight.Bulb(self._ipaddr,
+ model=self._model)
self._bulb_device.get_properties() # force init for type
self._available = True
@@ -305,6 +310,15 @@ class YeelightLight(Light):
if self._bulb_device.bulb_type == yeelight.BulbType.Color:
self._supported_features = SUPPORT_YEELIGHT_RGB
+ elif self._bulb_device.bulb_type == yeelight.BulbType.WhiteTemp:
+ self._supported_features = SUPPORT_YEELIGHT_WHITE_TEMP
+
+ if self._min_mireds is None:
+ model_specs = self._bulb.get_model_specs()
+ self._min_mireds = \
+ kelvin_to_mired(model_specs['color_temp']['max'])
+ self._max_mireds = \
+ kelvin_to_mired(model_specs['color_temp']['min'])
self._is_on = self._properties.get('power') == 'on'
@@ -500,5 +514,6 @@ class YeelightLight(Light):
import yeelight
try:
self._bulb.set_power_mode(yeelight.enums.PowerMode[mode.upper()])
+ self.async_schedule_update_ha_state(True)
except yeelight.BulbException as ex:
_LOGGER.error("Unable to set the power mode: %s", ex)
diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py
index 1e768eb127a..09f3709f216 100644
--- a/homeassistant/components/light/zwave.py
+++ b/homeassistant/components/light/zwave.py
@@ -7,13 +7,14 @@ https://home-assistant.io/components/light.zwave/
import logging
from threading import Timer
+from homeassistant.core import callback
from homeassistant.components.light import (
ATTR_WHITE_VALUE, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR,
ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR,
SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, DOMAIN, Light)
from homeassistant.components import zwave
-from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import
from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__)
@@ -43,6 +44,22 @@ TEMP_WARM_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 * 2 + TEMP_COLOR_MIN
TEMP_COLD_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 + TEMP_COLOR_MIN
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Old method of setting up Z-Wave lights."""
+ pass
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Z-Wave Light from Config Entry."""
+ @callback
+ def async_add_light(light):
+ """Add Z-Wave Light."""
+ async_add_entities([light])
+
+ async_dispatcher_connect(hass, 'zwave_new_light', async_add_light)
+
+
def get_device(node, values, node_config, **kwargs):
"""Create Z-Wave entity device."""
refresh = node_config.get(zwave.CONF_REFRESH_VALUE)
diff --git a/homeassistant/components/lock/august.py b/homeassistant/components/lock/august.py
index 7aec3c78690..e8949255ee9 100644
--- a/homeassistant/components/lock/august.py
+++ b/homeassistant/components/lock/august.py
@@ -4,12 +4,15 @@ Support for August lock.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/lock.august/
"""
+import logging
from datetime import timedelta
from homeassistant.components.august import DATA_AUGUST
from homeassistant.components.lock import LockDevice
from homeassistant.const import ATTR_BATTERY_LEVEL
+_LOGGER = logging.getLogger(__name__)
+
DEPENDENCIES = ['august']
SCAN_INTERVAL = timedelta(seconds=5)
@@ -21,6 +24,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
devices = []
for lock in data.locks:
+ _LOGGER.debug("Adding lock for %s", lock.device_name)
devices.append(AugustLock(data, lock))
add_entities(devices, True)
@@ -77,6 +81,9 @@ class AugustLock(LockDevice):
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
+ if self._lock_detail is None:
+ return None
+
return {
ATTR_BATTERY_LEVEL: self._lock_detail.battery_level,
}
diff --git a/homeassistant/components/lock/template.py b/homeassistant/components/lock/template.py
new file mode 100644
index 00000000000..527af4c5b85
--- /dev/null
+++ b/homeassistant/components/lock/template.py
@@ -0,0 +1,141 @@
+"""
+Support for locks which integrates with other components.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/lock.template/
+"""
+import logging
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+
+from homeassistant.core import callback
+from homeassistant.components.lock import (LockDevice, PLATFORM_SCHEMA)
+from homeassistant.const import (
+ CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE,
+ EVENT_HOMEASSISTANT_START, STATE_ON, STATE_LOCKED, MATCH_ALL)
+from homeassistant.exceptions import TemplateError
+from homeassistant.helpers.event import async_track_state_change
+from homeassistant.helpers.script import Script
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_LOCK = 'lock'
+CONF_UNLOCK = 'unlock'
+
+DEFAULT_NAME = 'Template Lock'
+DEFAULT_OPTIMISTIC = False
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA,
+ vol.Required(CONF_UNLOCK): cv.SCRIPT_SCHEMA,
+ vol.Required(CONF_VALUE_TEMPLATE): cv.template,
+ vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean
+})
+
+
+async def async_setup_platform(hass, config, async_add_devices,
+ discovery_info=None):
+ """Set up the Template lock."""
+ name = config.get(CONF_NAME)
+ value_template = config.get(CONF_VALUE_TEMPLATE)
+ value_template.hass = hass
+ value_template_entity_ids = value_template.extract_entities()
+
+ if value_template_entity_ids == MATCH_ALL:
+ _LOGGER.warning(
+ 'Template lock %s has no entity ids configured to track nor '
+ 'were we able to extract the entities to track from the %s '
+ 'template. This entity will only be able to be updated '
+ 'manually.', name, CONF_VALUE_TEMPLATE)
+
+ async_add_devices([TemplateLock(
+ hass,
+ name,
+ value_template,
+ value_template_entity_ids,
+ config.get(CONF_LOCK),
+ config.get(CONF_UNLOCK),
+ config.get(CONF_OPTIMISTIC)
+ )])
+
+
+class TemplateLock(LockDevice):
+ """Representation of a template lock."""
+
+ def __init__(self, hass, name, value_template, entity_ids,
+ command_lock, command_unlock, optimistic):
+ """Initialize the lock."""
+ self._state = None
+ self._hass = hass
+ self._name = name
+ self._state_template = value_template
+ self._state_entities = entity_ids
+ self._command_lock = Script(hass, command_lock)
+ self._command_unlock = Script(hass, command_unlock)
+ self._optimistic = optimistic
+
+ async def async_added_to_hass(self):
+ """Register callbacks."""
+ @callback
+ def template_lock_state_listener(entity, old_state, new_state):
+ """Handle target device state changes."""
+ self.async_schedule_update_ha_state(True)
+
+ @callback
+ def template_lock_startup(event):
+ """Update template on startup."""
+ if self._state_entities != MATCH_ALL:
+ # Track state change only for valid templates
+ async_track_state_change(
+ self._hass, self._state_entities,
+ template_lock_state_listener)
+ self.async_schedule_update_ha_state(True)
+
+ self._hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_START, template_lock_startup)
+
+ @property
+ def assumed_state(self):
+ """Return true if we do optimistic updates."""
+ return self._optimistic
+
+ @property
+ def should_poll(self):
+ """No polling needed."""
+ return False
+
+ @property
+ def name(self):
+ """Return the name of the lock."""
+ return self._name
+
+ @property
+ def is_locked(self):
+ """Return true if lock is locked."""
+ return self._state
+
+ async def async_update(self):
+ """Update the state from the template."""
+ try:
+ self._state = self._state_template.async_render().lower() in (
+ 'true', STATE_ON, STATE_LOCKED)
+ except TemplateError as ex:
+ self._state = None
+ _LOGGER.error('Could not render template %s: %s', self._name, ex)
+
+ async def async_lock(self, **kwargs):
+ """Lock the device."""
+ if self._optimistic:
+ self._state = True
+ self.async_schedule_update_ha_state()
+ await self._command_lock.async_run(context=self._context)
+
+ async def async_unlock(self, **kwargs):
+ """Unlock the device."""
+ if self._optimistic:
+ self._state = False
+ self.async_schedule_update_ha_state()
+ await self._command_unlock.async_run(context=self._context)
diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py
index 5cbd2b9432b..5bd7ed0d2f5 100644
--- a/homeassistant/components/logbook.py
+++ b/homeassistant/components/logbook.py
@@ -14,13 +14,16 @@ from homeassistant.loader import bind_hass
from homeassistant.components import sun
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
- ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, CONF_EXCLUDE,
- CONF_INCLUDE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
- EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, HTTP_BAD_REQUEST, STATE_NOT_HOME,
- STATE_OFF, STATE_ON)
+ ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, ATTR_SERVICE,
+ CONF_EXCLUDE, CONF_INCLUDE, EVENT_HOMEASSISTANT_START,
+ EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED,
+ HTTP_BAD_REQUEST, STATE_NOT_HOME, STATE_OFF, STATE_ON)
from homeassistant.core import (
DOMAIN as HA_DOMAIN, State, callback, split_entity_id)
from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME
+from homeassistant.components.homekit.const import (
+ ATTR_DISPLAY_NAME, ATTR_VALUE, DOMAIN as DOMAIN_HOMEKIT,
+ EVENT_HOMEKIT_CHANGED)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
@@ -56,7 +59,7 @@ CONFIG_SCHEMA = vol.Schema({
ALL_EVENT_TYPES = [
EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
- EVENT_ALEXA_SMART_HOME
+ EVENT_ALEXA_SMART_HOME, EVENT_HOMEKIT_CHANGED
]
LOG_MESSAGE_SCHEMA = vol.Schema({
@@ -294,6 +297,25 @@ def humanify(hass, events):
'context_user_id': event.context.user_id
}
+ elif event.event_type == EVENT_HOMEKIT_CHANGED:
+ data = event.data
+ entity_id = data.get(ATTR_ENTITY_ID)
+ value = data.get(ATTR_VALUE)
+
+ value_msg = " to {}".format(value) if value else ''
+ message = "send command {}{} for {}".format(
+ data[ATTR_SERVICE], value_msg, data[ATTR_DISPLAY_NAME])
+
+ yield {
+ 'when': event.time_fired,
+ 'name': 'HomeKit',
+ 'message': message,
+ 'domain': DOMAIN_HOMEKIT,
+ 'entity_id': entity_id,
+ 'context_id': event.context.id,
+ 'context_user_id': event.context.user_id
+ }
+
def _get_events(hass, config, start_day, end_day, entity_id=None):
"""Get events for a period of time."""
diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py
index a24c8eb9e91..141f3c98334 100644
--- a/homeassistant/components/lovelace/__init__.py
+++ b/homeassistant/components/lovelace/__init__.py
@@ -1,19 +1,240 @@
"""Lovelace UI."""
+import logging
+import uuid
+import os
+from os import O_CREAT, O_TRUNC, O_WRONLY
+from collections import OrderedDict
+from typing import Dict, List, Union
+
import voluptuous as vol
from homeassistant.components import websocket_api
-from homeassistant.util.yaml import load_yaml
from homeassistant.exceptions import HomeAssistantError
+_LOGGER = logging.getLogger(__name__)
DOMAIN = 'lovelace'
+REQUIREMENTS = ['ruamel.yaml==0.15.72']
+
+LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml'
+JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name
+
+FORMAT_YAML = 'yaml'
+FORMAT_JSON = 'json'
OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config'
WS_TYPE_GET_LOVELACE_UI = 'lovelace/config'
+
+WS_TYPE_GET_CARD = 'lovelace/config/card/get'
+WS_TYPE_UPDATE_CARD = 'lovelace/config/card/update'
+WS_TYPE_ADD_CARD = 'lovelace/config/card/add'
+
SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI,
OLD_WS_TYPE_GET_LOVELACE_UI),
})
+SCHEMA_GET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+ vol.Required('type'): WS_TYPE_GET_CARD,
+ vol.Required('card_id'): str,
+ vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON,
+ FORMAT_YAML),
+})
+
+SCHEMA_UPDATE_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+ vol.Required('type'): WS_TYPE_UPDATE_CARD,
+ vol.Required('card_id'): str,
+ vol.Required('card_config'): vol.Any(str, Dict),
+ vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON,
+ FORMAT_YAML),
+})
+
+SCHEMA_ADD_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+ vol.Required('type'): WS_TYPE_ADD_CARD,
+ vol.Required('view_id'): str,
+ vol.Required('card_config'): vol.Any(str, Dict),
+ vol.Optional('position'): int,
+ vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON,
+ FORMAT_YAML),
+})
+
+
+class WriteError(HomeAssistantError):
+ """Error writing the data."""
+
+
+class CardNotFoundError(HomeAssistantError):
+ """Card not found in data."""
+
+
+class ViewNotFoundError(HomeAssistantError):
+ """View not found in data."""
+
+
+class UnsupportedYamlError(HomeAssistantError):
+ """Unsupported YAML."""
+
+
+def save_yaml(fname: str, data: JSON_TYPE):
+ """Save a YAML file."""
+ from ruamel.yaml import YAML
+ from ruamel.yaml.error import YAMLError
+ yaml = YAML(typ='rt')
+ yaml.indent(sequence=4, offset=2)
+ tmp_fname = fname + "__TEMP__"
+ try:
+ with open(os.open(tmp_fname, O_WRONLY | O_CREAT | O_TRUNC, 0o644),
+ 'w', encoding='utf-8') as temp_file:
+ yaml.dump(data, temp_file)
+ os.replace(tmp_fname, fname)
+ except YAMLError as exc:
+ _LOGGER.error(str(exc))
+ raise HomeAssistantError(exc)
+ except OSError as exc:
+ _LOGGER.exception('Saving YAML file %s failed: %s', fname, exc)
+ raise WriteError(exc)
+ finally:
+ if os.path.exists(tmp_fname):
+ try:
+ os.remove(tmp_fname)
+ except OSError as exc:
+ # If we are cleaning up then something else went wrong, so
+ # we should suppress likely follow-on errors in the cleanup
+ _LOGGER.error("YAML replacement cleanup failed: %s", exc)
+
+
+def _yaml_unsupported(loader, node):
+ raise UnsupportedYamlError(
+ 'Unsupported YAML, you can not use {} in ui-lovelace.yaml'
+ .format(node.tag))
+
+
+def load_yaml(fname: str) -> JSON_TYPE:
+ """Load a YAML file."""
+ from ruamel.yaml import YAML
+ from ruamel.yaml.constructor import RoundTripConstructor
+ from ruamel.yaml.error import YAMLError
+
+ RoundTripConstructor.add_constructor(None, _yaml_unsupported)
+
+ yaml = YAML(typ='rt')
+
+ try:
+ with open(fname, encoding='utf-8') as conf_file:
+ # If configuration file is empty YAML returns None
+ # We convert that to an empty dict
+ return yaml.load(conf_file) or OrderedDict()
+ except YAMLError as exc:
+ _LOGGER.error("YAML error in %s: %s", fname, exc)
+ raise HomeAssistantError(exc)
+ except UnicodeDecodeError as exc:
+ _LOGGER.error("Unable to read file %s: %s", fname, exc)
+ raise HomeAssistantError(exc)
+
+
+def load_config(fname: str) -> JSON_TYPE:
+ """Load a YAML file and adds id to views and cards if not present."""
+ config = load_yaml(fname)
+ # Check if all views and cards have an id or else add one
+ updated = False
+ index = 0
+ for view in config.get('views', []):
+ if 'id' not in view:
+ updated = True
+ view.insert(0, 'id', index,
+ comment="Automatically created id")
+ for card in view.get('cards', []):
+ if 'id' not in card:
+ updated = True
+ card.insert(0, 'id', uuid.uuid4().hex,
+ comment="Automatically created id")
+ index += 1
+ if updated:
+ save_yaml(fname, config)
+ return config
+
+
+def object_to_yaml(data: JSON_TYPE) -> str:
+ """Create yaml string from object."""
+ from ruamel.yaml import YAML
+ from ruamel.yaml.error import YAMLError
+ from ruamel.yaml.compat import StringIO
+ yaml = YAML(typ='rt')
+ yaml.indent(sequence=4, offset=2)
+ stream = StringIO()
+ try:
+ yaml.dump(data, stream)
+ return stream.getvalue()
+ except YAMLError as exc:
+ _LOGGER.error("YAML error: %s", exc)
+ raise HomeAssistantError(exc)
+
+
+def yaml_to_object(data: str) -> JSON_TYPE:
+ """Create object from yaml string."""
+ from ruamel.yaml import YAML
+ from ruamel.yaml.error import YAMLError
+ yaml = YAML(typ='rt')
+ try:
+ return yaml.load(data)
+ except YAMLError as exc:
+ _LOGGER.error("YAML error: %s", exc)
+ raise HomeAssistantError(exc)
+
+
+def get_card(fname: str, card_id: str, data_format: str = FORMAT_YAML)\
+ -> JSON_TYPE:
+ """Load a specific card config for id."""
+ config = load_yaml(fname)
+ for view in config.get('views', []):
+ for card in view.get('cards', []):
+ if card.get('id') != card_id:
+ continue
+ if data_format == FORMAT_YAML:
+ return object_to_yaml(card)
+ return card
+
+ raise CardNotFoundError(
+ "Card with ID: {} was not found in {}.".format(card_id, fname))
+
+
+def update_card(fname: str, card_id: str, card_config: str,
+ data_format: str = FORMAT_YAML):
+ """Save a specific card config for id."""
+ config = load_yaml(fname)
+ for view in config.get('views', []):
+ for card in view.get('cards', []):
+ if card.get('id') != card_id:
+ continue
+ if data_format == FORMAT_YAML:
+ card_config = yaml_to_object(card_config)
+ card.update(card_config)
+ save_yaml(fname, config)
+ return
+
+ raise CardNotFoundError(
+ "Card with ID: {} was not found in {}.".format(card_id, fname))
+
+
+def add_card(fname: str, view_id: str, card_config: str,
+ position: int = None, data_format: str = FORMAT_YAML):
+ """Add a card to a view."""
+ config = load_yaml(fname)
+ for view in config.get('views', []):
+ if view.get('id') != view_id:
+ continue
+ cards = view.get('cards', [])
+ if data_format == FORMAT_YAML:
+ card_config = yaml_to_object(card_config)
+ if position is None:
+ cards.append(card_config)
+ else:
+ cards.insert(position, card_config)
+ save_yaml(fname, config)
+ return
+
+ raise ViewNotFoundError(
+ "View with ID: {} was not found in {}.".format(view_id, fname))
+
async def async_setup(hass, config):
"""Set up the Lovelace commands."""
@@ -26,6 +247,18 @@ async def async_setup(hass, config):
WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config,
SCHEMA_GET_LOVELACE_UI)
+ hass.components.websocket_api.async_register_command(
+ WS_TYPE_GET_CARD, websocket_lovelace_get_card,
+ SCHEMA_GET_CARD)
+
+ hass.components.websocket_api.async_register_command(
+ WS_TYPE_UPDATE_CARD, websocket_lovelace_update_card,
+ SCHEMA_UPDATE_CARD)
+
+ hass.components.websocket_api.async_register_command(
+ WS_TYPE_ADD_CARD, websocket_lovelace_add_card,
+ SCHEMA_ADD_CARD)
+
return True
@@ -35,13 +268,15 @@ async def websocket_lovelace_config(hass, connection, msg):
error = None
try:
config = await hass.async_add_executor_job(
- load_yaml, hass.config.path('ui-lovelace.yaml'))
+ load_config, hass.config.path(LOVELACE_CONFIG_FILE))
message = websocket_api.result_message(
msg['id'], config
)
except FileNotFoundError:
error = ('file_not_found',
'Could not find ui-lovelace.yaml in your config dir.')
+ except UnsupportedYamlError as err:
+ error = 'unsupported_error', str(err)
except HomeAssistantError as err:
error = 'load_error', str(err)
@@ -49,3 +284,85 @@ async def websocket_lovelace_config(hass, connection, msg):
message = websocket_api.error_message(msg['id'], *error)
connection.send_message(message)
+
+
+@websocket_api.async_response
+async def websocket_lovelace_get_card(hass, connection, msg):
+ """Send lovelace card config over websocket config."""
+ error = None
+ try:
+ card = await hass.async_add_executor_job(
+ get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'],
+ msg.get('format', FORMAT_YAML))
+ message = websocket_api.result_message(
+ msg['id'], card
+ )
+ except FileNotFoundError:
+ error = ('file_not_found',
+ 'Could not find ui-lovelace.yaml in your config dir.')
+ except UnsupportedYamlError as err:
+ error = 'unsupported_error', str(err)
+ except CardNotFoundError as err:
+ error = 'card_not_found', str(err)
+ except HomeAssistantError as err:
+ error = 'load_error', str(err)
+
+ if error is not None:
+ message = websocket_api.error_message(msg['id'], *error)
+
+ connection.send_message(message)
+
+
+@websocket_api.async_response
+async def websocket_lovelace_update_card(hass, connection, msg):
+ """Receive lovelace card config over websocket and save."""
+ error = None
+ try:
+ await hass.async_add_executor_job(
+ update_card, hass.config.path(LOVELACE_CONFIG_FILE),
+ msg['card_id'], msg['card_config'], msg.get('format', FORMAT_YAML))
+ message = websocket_api.result_message(
+ msg['id'], True
+ )
+ except FileNotFoundError:
+ error = ('file_not_found',
+ 'Could not find ui-lovelace.yaml in your config dir.')
+ except UnsupportedYamlError as err:
+ error = 'unsupported_error', str(err)
+ except CardNotFoundError as err:
+ error = 'card_not_found', str(err)
+ except HomeAssistantError as err:
+ error = 'save_error', str(err)
+
+ if error is not None:
+ message = websocket_api.error_message(msg['id'], *error)
+
+ connection.send_message(message)
+
+
+@websocket_api.async_response
+async def websocket_lovelace_add_card(hass, connection, msg):
+ """Add new card to view over websocket and save."""
+ error = None
+ try:
+ await hass.async_add_executor_job(
+ add_card, hass.config.path(LOVELACE_CONFIG_FILE),
+ msg['view_id'], msg['card_config'], msg.get('position'),
+ msg.get('format', FORMAT_YAML))
+ message = websocket_api.result_message(
+ msg['id'], True
+ )
+ except FileNotFoundError:
+ error = ('file_not_found',
+ 'Could not find ui-lovelace.yaml in your config dir.')
+ except UnsupportedYamlError as err:
+ error = 'unsupported_error', str(err)
+ except ViewNotFoundError as err:
+ error = 'view_not_found', str(err)
+ except HomeAssistantError as err:
+ error = 'save_error', str(err)
+
+ if error is not None:
+ message = websocket_api.error_message(msg['id'], *error)
+
+ connection.send_message(message)
diff --git a/homeassistant/components/mailgun/.translations/ca.json b/homeassistant/components/mailgun/.translations/ca.json
new file mode 100644
index 00000000000..d644b9b8c73
--- /dev/null
+++ b/homeassistant/components/mailgun/.translations/ca.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "La vostra inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Mailgun.",
+ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
+ },
+ "create_entry": {
+ "default": "Per enviar esdeveniments a Home Assistant, haureu de configurar [Webhooks amb Mailgun] ({mailgun_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - M\u00e8tode: POST \n - Tipus de contingut: application/x-www-form-urlencoded\n\nConsulteu [la documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar les dades entrants."
+ },
+ "step": {
+ "user": {
+ "description": "Esteu segur que voleu configurar Mailgun?",
+ "title": "Configureu el Webhook de Mailgun"
+ }
+ },
+ "title": "Mailgun"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/.translations/en.json b/homeassistant/components/mailgun/.translations/en.json
new file mode 100644
index 00000000000..3abb8aba726
--- /dev/null
+++ b/homeassistant/components/mailgun/.translations/en.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Mailgun messages.",
+ "one_instance_allowed": "Only a single instance is necessary."
+ },
+ "create_entry": {
+ "default": "To send events to Home Assistant, you will need to setup [Webhooks with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
+ },
+ "step": {
+ "user": {
+ "description": "Are you sure you want to set up Mailgun?",
+ "title": "Set up the Mailgun Webhook"
+ }
+ },
+ "title": "Mailgun"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/.translations/ko.json b/homeassistant/components/mailgun/.translations/ko.json
new file mode 100644
index 00000000000..0dd8cbdb47d
--- /dev/null
+++ b/homeassistant/components/mailgun/.translations/ko.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Mailgun \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c\ud569\ub2c8\ub2e4.",
+ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4."
+ },
+ "create_entry": {
+ "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Mailgun Webhook]({mailgun_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/x-www-form-urlencoded\n \n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694."
+ },
+ "step": {
+ "user": {
+ "description": "Mailgun \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "Mailgun Webhook \uc124\uc815"
+ }
+ },
+ "title": "Mailgun"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/.translations/lb.json b/homeassistant/components/mailgun/.translations/lb.json
new file mode 100644
index 00000000000..f84225444d9
--- /dev/null
+++ b/homeassistant/components/mailgun/.translations/lb.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Mailgun Noriichten z'empf\u00e4nken.",
+ "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
+ },
+ "create_entry": {
+ "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, mussen [Webhooks mat Mailgun]({mailgun_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nLiest [Dokumentatioun]({docs_url}) w\u00e9i een Automatiounen ariicht welch eingehend Donn\u00e9\u00eb trait\u00e9ieren."
+ },
+ "step": {
+ "user": {
+ "description": "S\u00e9cher fir Mailgun anzeriichten?",
+ "title": "Mailgun Webhook ariichten"
+ }
+ },
+ "title": "Mailgun"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/.translations/nl.json b/homeassistant/components/mailgun/.translations/nl.json
new file mode 100644
index 00000000000..d71c311b7f8
--- /dev/null
+++ b/homeassistant/components/mailgun/.translations/nl.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Uw Home Assistant instantie moet toegankelijk zijn vanaf het internet om Mailgun-berichten te ontvangen.",
+ "one_instance_allowed": "Slechts \u00e9\u00e9n enkele instantie is nodig."
+ },
+ "step": {
+ "user": {
+ "description": "Weet u zeker dat u Mailgun wilt instellen?",
+ "title": "Stel de Mailgun Webhook in"
+ }
+ },
+ "title": "Mailgun"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/.translations/pl.json b/homeassistant/components/mailgun/.translations/pl.json
new file mode 100644
index 00000000000..ba89efab0c2
--- /dev/null
+++ b/homeassistant/components/mailgun/.translations/pl.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty Mailgun.",
+ "one_instance_allowed": "Wymagana jest tylko jedna instancja."
+ },
+ "create_entry": {
+ "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Mailgun Webhook]({mailgun_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/x-www-form-urlencoded \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane."
+ },
+ "step": {
+ "user": {
+ "description": "Czy chcesz skonfigurowa\u0107 Mailgun?",
+ "title": "Konfiguracja Mailgun Webhook"
+ }
+ },
+ "title": "Mailgun"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/.translations/pt.json b/homeassistant/components/mailgun/.translations/pt.json
new file mode 100644
index 00000000000..963d3322d84
--- /dev/null
+++ b/homeassistant/components/mailgun/.translations/pt.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "A sua inst\u00e2ncia Home Assistent precisa de ser acess\u00edvel a partir da internet para receber mensagens Mailgun.",
+ "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria."
+ },
+ "create_entry": {
+ "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar [Webhooks with Mailgun] ({mailgun_url}). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application/x-www-form-urlencoded \n\n Veja [a documenta\u00e7\u00e3o] ({docs_url}) sobre como configurar automa\u00e7\u00f5es para manipular dados de entrada."
+ },
+ "step": {
+ "user": {
+ "description": "Tem certeza de que deseja configurar o Mailgun?",
+ "title": "Configurar o Mailgun Webhook"
+ }
+ },
+ "title": "Mailgun"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/.translations/ru.json b/homeassistant/components/mailgun/.translations/ru.json
new file mode 100644
index 00000000000..62007a95809
--- /dev/null
+++ b/homeassistant/components/mailgun/.translations/ru.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Mailgun.",
+ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ },
+ "create_entry": {
+ "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Mailgun]({mailgun_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445."
+ },
+ "step": {
+ "user": {
+ "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Mailgun?",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Mailgun Webhook"
+ }
+ },
+ "title": "Mailgun"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/.translations/sl.json b/homeassistant/components/mailgun/.translations/sl.json
new file mode 100644
index 00000000000..12dad4d8c7e
--- /dev/null
+++ b/homeassistant/components/mailgun/.translations/sl.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": " \u010ce \u017eelite prejemati sporo\u010dila Mailgun, mora biti Home Assistent dostopen prek interneta.",
+ "one_instance_allowed": "Potrebna je samo ena instanca."
+ },
+ "create_entry": {
+ "default": "Za po\u0161iljanje dogodkov Home Assistentu boste morali nastaviti [Webhooks z Mailgun]({mailgun_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) o tem, kako nastavite automations za obravnavo dohodnih podatkov."
+ },
+ "step": {
+ "user": {
+ "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti Mailgun?",
+ "title": "Nastavite Mailgun Webhook"
+ }
+ },
+ "title": "Mailgun"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/.translations/zh-Hant.json b/homeassistant/components/mailgun/.translations/zh-Hant.json
new file mode 100644
index 00000000000..4b9ab3a7abb
--- /dev/null
+++ b/homeassistant/components/mailgun/.translations/zh-Hant.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Mailgun \u8a0a\u606f\u3002",
+ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002"
+ },
+ "create_entry": {
+ "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8a2d\u5b9a [Webhooks with Mailgun]({mailgun_url}) \u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002"
+ },
+ "step": {
+ "user": {
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a Mailgun\uff1f",
+ "title": "\u8a2d\u5b9a Mailgun Webhook"
+ }
+ },
+ "title": "Mailgun"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py
index ffcb0f6ab95..401fd729f9c 100644
--- a/homeassistant/components/media_extractor.py
+++ b/homeassistant/components/media_extractor.py
@@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA)
from homeassistant.helpers import config_validation as cv
-REQUIREMENTS = ['youtube_dl==2018.09.26']
+REQUIREMENTS = ['youtube_dl==2018.10.05']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py
index 296548dd3c2..bf934311303 100644
--- a/homeassistant/components/media_player/denonavr.py
+++ b/homeassistant/components/media_player/denonavr.py
@@ -21,7 +21,7 @@ from homeassistant.const import (
STATE_PAUSED, STATE_PLAYING)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['denonavr==0.7.5']
+REQUIREMENTS = ['denonavr==0.7.6']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py
index b787ed689c8..bf3fce97650 100644
--- a/homeassistant/components/media_player/dlna_dmr.py
+++ b/homeassistant/components/media_player/dlna_dmr.py
@@ -25,7 +25,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.util import get_local_ip
-REQUIREMENTS = ['async-upnp-client==0.12.6']
+REQUIREMENTS = ['async-upnp-client==0.12.7']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/gstreamer.py b/homeassistant/components/media_player/gstreamer.py
index e520fcb1033..fa8545bae03 100644
--- a/homeassistant/components/media_player/gstreamer.py
+++ b/homeassistant/components/media_player/gstreamer.py
@@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['gstreamer-player==1.1.0']
+REQUIREMENTS = ['gstreamer-player==1.1.2']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/horizon.py b/homeassistant/components/media_player/horizon.py
index 04471c69b9c..058796ea46d 100644
--- a/homeassistant/components/media_player/horizon.py
+++ b/homeassistant/components/media_player/horizon.py
@@ -92,9 +92,12 @@ class HorizonDevice(MediaPlayerDevice):
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update(self):
"""Update State using the media server running on the Horizon."""
- if self._client.is_powered_on():
- self._state = STATE_PLAYING
- else:
+ try:
+ if self._client.is_powered_on():
+ self._state = STATE_PLAYING
+ else:
+ self._state = STATE_OFF
+ except OSError:
self._state = STATE_OFF
def turn_on(self):
diff --git a/homeassistant/components/media_player/lg_soundbar.py b/homeassistant/components/media_player/lg_soundbar.py
new file mode 100644
index 00000000000..38b27bd074a
--- /dev/null
+++ b/homeassistant/components/media_player/lg_soundbar.py
@@ -0,0 +1,196 @@
+"""
+Support for LG soundbars.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/media_player.lg_soundbar/
+"""
+import logging
+
+from homeassistant.components.media_player import (
+ SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
+ SUPPORT_SELECT_SOUND_MODE, MediaPlayerDevice)
+
+from homeassistant.const import STATE_ON
+
+REQUIREMENTS = ['temescal==0.1']
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORT_LG = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE \
+ | SUPPORT_SELECT_SOUND_MODE
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the LG platform."""
+ if discovery_info is not None:
+ add_entities([LGDevice(discovery_info)], True)
+
+
+class LGDevice(MediaPlayerDevice):
+ """Representation of an LG soundbar device."""
+
+ def __init__(self, discovery_info):
+ """Initialize the LG speakers."""
+ import temescal
+
+ host = discovery_info.get('host')
+ port = discovery_info.get('port')
+
+ self._name = ""
+ self._volume = 0
+ self._volume_min = 0
+ self._volume_max = 0
+ self._function = -1
+ self._functions = []
+ self._equaliser = -1
+ self._equalisers = []
+ self._mute = 0
+ self._rear_volume = 0
+ self._rear_volume_min = 0
+ self._rear_volume_max = 0
+ self._woofer_volume = 0
+ self._woofer_volume_min = 0
+ self._woofer_volume_max = 0
+ self._bass = 0
+ self._treble = 0
+
+ self._device = temescal.temescal(host, port=port,
+ callback=self.handle_event)
+ self.update()
+
+ def handle_event(self, response):
+ """Handle responses from the speakers."""
+ data = response['data']
+ if response['msg'] == "EQ_VIEW_INFO":
+ if 'i_bass' in data:
+ self._bass = data['i_bass']
+ if 'i_treble' in data:
+ self._treble = data['i_treble']
+ if 'ai_eq_list' in data:
+ self._equalisers = data['ai_eq_list']
+ if 'i_curr_eq' in data:
+ self._equaliser = data['i_curr_eq']
+ elif response['msg'] == "SPK_LIST_VIEW_INFO":
+ if 'i_vol' in data:
+ self._volume = data['i_vol']
+ if 's_user_name' in data:
+ self._name = data['s_user_name']
+ if 'i_vol_min' in data:
+ self._volume_min = data['i_vol_min']
+ if 'i_vol_max' in data:
+ self._volume_max = data['i_vol_max']
+ if 'b_mute' in data:
+ self._mute = data['b_mute']
+ if 'i_curr_func' in data:
+ self._function = data['i_curr_func']
+ elif response['msg'] == "FUNC_VIEW_INFO":
+ if 'i_curr_func' in data:
+ self._function = data['i_curr_func']
+ if 'ai_func_list' in data:
+ self._functions = data['ai_func_list']
+ elif response['msg'] == "SETTING_VIEW_INFO":
+ if 'i_rear_min' in data:
+ self._rear_volume_min = data['i_rear_min']
+ if 'i_rear_max' in data:
+ self._rear_volume_max = data['i_rear_max']
+ if 'i_rear_level' in data:
+ self._rear_volume = data['i_rear_level']
+ if 'i_woofer_min' in data:
+ self._woofer_volume_min = data['i_woofer_min']
+ if 'i_woofer_max' in data:
+ self._woofer_volume_max = data['i_woofer_max']
+ if 'i_woofer_level' in data:
+ self._woofer_volume = data['i_woofer_level']
+ if 'i_curr_eq' in data:
+ self._equaliser = data['i_curr_eq']
+ if 's_user_name' in data:
+ self._name = data['s_user_name']
+ self.schedule_update_ha_state()
+
+ def update(self):
+ """Trigger updates from the device."""
+ self._device.get_eq()
+ self._device.get_info()
+ self._device.get_func()
+ self._device.get_settings()
+ self._device.get_product_info()
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._name
+
+ @property
+ def volume_level(self):
+ """Volume level of the media player (0..1)."""
+ if self._volume_max != 0:
+ return self._volume/self._volume_max
+ return 0
+
+ @property
+ def is_volume_muted(self):
+ """Boolean if volume is currently muted."""
+ return self._mute
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ return STATE_ON
+
+ @property
+ def sound_mode(self):
+ """Return the current sound mode."""
+ import temescal
+ if self._equaliser == -1:
+ return ""
+ return temescal.equalisers[self._equaliser]
+
+ @property
+ def sound_mode_list(self):
+ """Return the available sound modes."""
+ import temescal
+ modes = []
+ for equaliser in self._equalisers:
+ modes.append(temescal.equalisers[equaliser])
+ return sorted(modes)
+
+ @property
+ def source(self):
+ """Return the current input source."""
+ import temescal
+ if self._function == -1:
+ return ""
+ return temescal.functions[self._function]
+
+ @property
+ def source_list(self):
+ """List of available input sources."""
+ import temescal
+ sources = []
+ for function in self._functions:
+ sources.append(temescal.functions[function])
+ return sorted(sources)
+
+ @property
+ def supported_features(self):
+ """Flag media player features that are supported."""
+ return SUPPORT_LG
+
+ def set_volume_level(self, volume):
+ """Set volume level, range 0..1."""
+ volume = volume * self._volume_max
+ self._device.set_volume(int(volume))
+
+ def mute_volume(self, mute):
+ """Mute (true) or unmute (false) media player."""
+ self._device.set_mute(mute)
+
+ def select_source(self, source):
+ """Select input source."""
+ import temescal
+ self._device.set_func(temescal.functions.index(source))
+
+ def select_sound_mode(self, sound_mode):
+ """Set Sound Mode for Receiver.."""
+ import temescal
+ self._device.set_eq(temescal.equalisers.index(sound_mode))
diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py
index 8953215f44e..b5eecd3d403 100644
--- a/homeassistant/components/media_player/mpd.py
+++ b/homeassistant/components/media_player/mpd.py
@@ -79,7 +79,7 @@ class MpdDevice(MediaPlayerDevice):
# set up MPD client
self._client = mpd.MPDClient()
- self._client.timeout = 5
+ self._client.timeout = 30
self._client.idletimeout = None
def _connect(self):
diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py
index 3a66aa66dc0..45d158a8653 100644
--- a/homeassistant/components/media_player/samsungtv.py
+++ b/homeassistant/components/media_player/samsungtv.py
@@ -8,8 +8,6 @@ import asyncio
from datetime import timedelta
import logging
import socket
-import subprocess
-import sys
import voluptuous as vol
@@ -19,8 +17,7 @@ from homeassistant.components.media_player import (
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
MediaPlayerDevice)
from homeassistant.const import (
- CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_TIMEOUT, STATE_OFF,
- STATE_ON, STATE_UNKNOWN)
+ CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_TIMEOUT, STATE_OFF)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import dt as dt_util
@@ -102,7 +99,7 @@ class SamsungTVDevice(MediaPlayerDevice):
self._muted = False
# Assume that the TV is in Play mode
self._playing = True
- self._state = STATE_UNKNOWN
+ self._state = None
self._remote = None
# Mark the end of a shutdown command (need to wait 15 seconds before
# sending the next command to avoid turning the TV back ON).
@@ -124,24 +121,7 @@ class SamsungTVDevice(MediaPlayerDevice):
def update(self):
"""Update state of device."""
- if sys.platform == 'win32':
- timeout_arg = '-w {}000'.format(self._config['timeout'])
- _ping_cmd = [
- 'ping', '-n 3', timeout_arg, self._config['host']]
- else:
- timeout_arg = '-W{}'.format(self._config['timeout'])
- _ping_cmd = [
- 'ping', '-n', '-q',
- '-c3', timeout_arg, self._config['host']]
-
- ping = subprocess.Popen(
- _ping_cmd,
- stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
- try:
- ping.communicate()
- self._state = STATE_ON if ping.returncode == 0 else STATE_OFF
- except subprocess.CalledProcessError:
- self._state = STATE_OFF
+ self.send_key("KEY")
def get_remote(self):
"""Create or return a remote control instance."""
@@ -168,11 +148,11 @@ class SamsungTVDevice(MediaPlayerDevice):
BrokenPipeError):
# BrokenPipe can occur when the commands is sent to fast
self._remote = None
- self._state = STATE_ON
+ self._state = None
except (self._exceptions_class.UnhandledResponse,
self._exceptions_class.AccessDenied):
# We got a response so it's on.
- self._state = STATE_ON
+ self._state = None
self._remote = None
_LOGGER.debug("Failed sending command %s", key, exc_info=True)
return
diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py
index 41ca1b4e85e..28ff269f400 100644
--- a/homeassistant/components/media_player/sonos.py
+++ b/homeassistant/components/media_player/sonos.py
@@ -447,16 +447,15 @@ class SonosDevice(MediaPlayerDevice):
"""Set available favorites."""
# SoCo 0.16 raises a generic Exception on invalid xml in favorites.
# Filter those out now so our list is safe to use.
- # pylint: disable=broad-except
try:
self._favorites = []
for fav in self.soco.music_library.get_sonos_favorites():
try:
if fav.reference.get_uri():
self._favorites.append(fav)
- except Exception:
+ except Exception: # pylint: disable=broad-except
_LOGGER.debug("Ignoring invalid favorite '%s'", fav.title)
- except Exception:
+ except Exception: # pylint: disable=broad-except
_LOGGER.debug("Ignoring invalid favorite list")
def _radio_artwork(self, url):
diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py
index de0f726c2ce..373d3c380fc 100644
--- a/homeassistant/components/media_player/volumio.py
+++ b/homeassistant/components/media_player/volumio.py
@@ -90,7 +90,6 @@ class Volumio(MediaPlayerDevice):
self._url = '{}:{}'.format(host, str(port))
self._name = name
self._state = {}
- self.async_update()
self._lastvol = self._state.get('volume', 0)
self._playlists = []
self._currentplaylist = None
@@ -113,7 +112,9 @@ class Volumio(MediaPlayerDevice):
return False
except (asyncio.TimeoutError, aiohttp.ClientError) as error:
- _LOGGER.error("Failed communicating with Volumio: %s", type(error))
+ _LOGGER.error(
+ "Failed communicating with Volumio '%s': %s",
+ self._name, type(error))
return False
try:
diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py
index 0a5b9fe509b..946e0517435 100644
--- a/homeassistant/components/media_player/webostv.py
+++ b/homeassistant/components/media_player/webostv.py
@@ -24,7 +24,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.script import Script
-REQUIREMENTS = ['pylgtv==0.1.7', 'websockets==6.0']
+REQUIREMENTS = ['pylgtv==0.1.9', 'websockets==6.0']
_CONFIGURING = {} # type: Dict[str, str]
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py
index be61560d52b..101dfc2bc53 100644
--- a/homeassistant/components/media_player/yamaha.py
+++ b/homeassistant/components/media_player/yamaha.py
@@ -12,9 +12,10 @@ import voluptuous as vol
from homeassistant.components.media_player import (
DOMAIN, MEDIA_PLAYER_SCHEMA, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA,
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA,
- SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE,
- SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE,
- SUPPORT_VOLUME_SET, MediaPlayerDevice)
+ SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP,
+ SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
+ SUPPORT_SELECT_SOUND_MODE, MediaPlayerDevice)
+
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_ON,
STATE_PLAYING)
@@ -184,8 +185,12 @@ class YamahaDevice(MediaPlayerDevice):
self._playback_support = self.receiver.get_playback_support()
self._is_playback_supported = self.receiver.is_playback_supported(
self._current_source)
- self._sound_mode = self.receiver.surround_program
- self._sound_mode_list = self.receiver.surround_programs()
+ if self._zone == "Main_Zone":
+ self._sound_mode = self.receiver.surround_program
+ self._sound_mode_list = self.receiver.surround_programs()
+ else:
+ self._sound_mode = None
+ self._sound_mode_list = None
def build_source_list(self):
"""Build the source list."""
diff --git a/homeassistant/components/media_player/yamaha_musiccast.py b/homeassistant/components/media_player/yamaha_musiccast.py
index bf21a3f5028..535a2ad01ca 100644
--- a/homeassistant/components/media_player/yamaha_musiccast.py
+++ b/homeassistant/components/media_player/yamaha_musiccast.py
@@ -39,7 +39,7 @@ DEFAULT_INTERVAL = 480
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.positive_int,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(INTERVAL_SECONDS, default=DEFAULT_INTERVAL): cv.positive_int,
})
diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py
index f484cb31a6c..c8d71af71b4 100644
--- a/homeassistant/components/modbus.py
+++ b/homeassistant/components/modbus.py
@@ -37,7 +37,7 @@ SERIAL_SCHEMA = {
ETHERNET_SCHEMA = {
vol.Required(CONF_HOST): cv.string,
- vol.Required(CONF_PORT): cv.positive_int,
+ vol.Required(CONF_PORT): cv.port,
vol.Required(CONF_TYPE): vol.Any('tcp', 'udp', 'rtuovertcp'),
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
}
diff --git a/homeassistant/components/mqtt/.translations/es.json b/homeassistant/components/mqtt/.translations/es.json
index e9e869ae966..182cce86057 100644
--- a/homeassistant/components/mqtt/.translations/es.json
+++ b/homeassistant/components/mqtt/.translations/es.json
@@ -2,6 +2,10 @@
"config": {
"step": {
"hassio_confirm": {
+ "data": {
+ "discovery": "Habilitar descubrimiento"
+ },
+ "description": "\u00bfDesea configurar Home Assistant para conectarse al agente MQTT provisto por el complemento hass.io {addon} ?",
"title": "MQTT Broker a trav\u00e9s del complemento Hass.io"
}
}
diff --git a/homeassistant/components/mqtt/.translations/hu.json b/homeassistant/components/mqtt/.translations/hu.json
index ba08d36d581..f08c601633e 100644
--- a/homeassistant/components/mqtt/.translations/hu.json
+++ b/homeassistant/components/mqtt/.translations/hu.json
@@ -10,6 +10,7 @@
"broker": {
"data": {
"broker": "Br\u00f3ker",
+ "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se",
"password": "Jelsz\u00f3",
"port": "Port",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
diff --git a/homeassistant/components/mqtt/.translations/pt.json b/homeassistant/components/mqtt/.translations/pt.json
index 1b8c3946b7c..21b9cbdf755 100644
--- a/homeassistant/components/mqtt/.translations/pt.json
+++ b/homeassistant/components/mqtt/.translations/pt.json
@@ -9,16 +9,23 @@
"step": {
"broker": {
"data": {
- "broker": "",
+ "broker": "Broker",
"discovery": "Ativar descoberta",
"password": "Palavra-passe",
"port": "Porto",
"username": "Utilizador"
},
"description": "Por favor, insira os detalhes de liga\u00e7\u00e3o ao seu broker MQTT.",
- "title": ""
+ "title": "MQTT"
+ },
+ "hassio_confirm": {
+ "data": {
+ "discovery": "Ativar descoberta"
+ },
+ "description": "Deseja configurar o Home Assistant para se ligar ao broker MQTT fornecido pelo add-on hass.io {addon}?",
+ "title": "MQTT Broker atrav\u00e9s do add-on Hass.io"
}
},
- "title": ""
+ "title": "MQTT"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/ro.json b/homeassistant/components/mqtt/.translations/ro.json
new file mode 100644
index 00000000000..bcd150e3063
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/ro.json
@@ -0,0 +1,31 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Este permis\u0103 numai o singur\u0103 configura\u021bie de MQTT."
+ },
+ "error": {
+ "cannot_connect": "Imposibil de conectat la broker."
+ },
+ "step": {
+ "broker": {
+ "data": {
+ "broker": "Broker",
+ "discovery": "Activa\u021bi descoperirea",
+ "password": "Parol\u0103",
+ "port": "Port",
+ "username": "Nume de utilizator"
+ },
+ "description": "Introduce\u021bi informa\u021biile de conectare ale brokerului dvs. MQTT.",
+ "title": "MQTT"
+ },
+ "hassio_confirm": {
+ "data": {
+ "discovery": "Activa\u021bi descoperirea"
+ },
+ "description": "Dori\u021bi s\u0103 configura\u021bi Home Assistant pentru a v\u0103 conecta la brokerul MQTT furnizat de addon-ul {addon} ?",
+ "title": "MQTT Broker, prin intermediul Hass.io add-on"
+ }
+ },
+ "title": "MQTT"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/.translations/tr.json b/homeassistant/components/mqtt/.translations/tr.json
new file mode 100644
index 00000000000..1b73b94d5a4
--- /dev/null
+++ b/homeassistant/components/mqtt/.translations/tr.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "hassio_confirm": {
+ "data": {
+ "discovery": "Ke\u015ffetmeyi etkinle\u015ftir"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index 3e25563e9ba..2f1895019dd 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -21,7 +21,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, CONF_PROTOCOL, CONF_USERNAME,
- CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP)
+ CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP, CONF_NAME)
from homeassistant.core import Event, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
@@ -73,6 +73,12 @@ CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available'
CONF_QOS = 'qos'
CONF_RETAIN = 'retain'
+CONF_IDENTIFIERS = 'identifiers'
+CONF_CONNECTIONS = 'connections'
+CONF_MANUFACTURER = 'manufacturer'
+CONF_MODEL = 'model'
+CONF_SW_VERSION = 'sw_version'
+
PROTOCOL_31 = '3.1'
PROTOCOL_311 = '3.1.1'
@@ -144,6 +150,15 @@ def valid_publish_topic(value: Any) -> str:
return value
+def validate_device_has_at_least_one_identifier(value: ConfigType) -> \
+ ConfigType:
+ """Validate that a device info entry has at least one identifying value."""
+ if not value.get(CONF_IDENTIFIERS) and not value.get(CONF_CONNECTIONS):
+ raise vol.Invalid("Device must have at least one identifying value in "
+ "'identifiers' and/or 'connections'")
+ return value
+
+
_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2]))
CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \
@@ -198,6 +213,17 @@ MQTT_AVAILABILITY_SCHEMA = vol.Schema({
default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string,
})
+MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All(vol.Schema({
+ vol.Optional(CONF_IDENTIFIERS, default=list):
+ vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(CONF_CONNECTIONS, default=list):
+ vol.All(cv.ensure_list, [vol.All(vol.Length(2), [cv.string])]),
+ vol.Optional(CONF_MANUFACTURER): cv.string,
+ vol.Optional(CONF_MODEL): cv.string,
+ vol.Optional(CONF_NAME): cv.string,
+ vol.Optional(CONF_SW_VERSION): cv.string,
+}), validate_device_has_at_least_one_identifier)
+
MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE)
# Sensor type platforms subscribe to MQTT events
@@ -866,3 +892,41 @@ class MqttDiscoveryUpdate(Entity):
self.hass,
MQTT_DISCOVERY_UPDATED.format(self._discovery_hash),
discovery_callback)
+
+
+class MqttEntityDeviceInfo(Entity):
+ """Mixin used for mqtt platforms that support the device registry."""
+
+ def __init__(self, device_config: Optional[ConfigType]) -> None:
+ """Initialize the device mixin."""
+ self._device_config = device_config
+
+ @property
+ def device_info(self):
+ """Return a device description for device registry."""
+ if not self._device_config:
+ return None
+
+ info = {
+ 'identifiers': {
+ (DOMAIN, id_)
+ for id_ in self._device_config[CONF_IDENTIFIERS]
+ },
+ 'connections': {
+ tuple(x) for x in self._device_config[CONF_CONNECTIONS]
+ }
+ }
+
+ if CONF_MANUFACTURER in self._device_config:
+ info['manufacturer'] = self._device_config[CONF_MANUFACTURER]
+
+ if CONF_MODEL in self._device_config:
+ info['model'] = self._device_config[CONF_MODEL]
+
+ if CONF_NAME in self._device_config:
+ info['name'] = self._device_config[CONF_NAME]
+
+ if CONF_SW_VERSION in self._device_config:
+ info['sw_version'] = self._device_config[CONF_SW_VERSION]
+
+ return info
diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py
index a762978a330..b8c8627c038 100644
--- a/homeassistant/components/mqtt/discovery.py
+++ b/homeassistant/components/mqtt/discovery.py
@@ -49,6 +49,7 @@ CONFIG_ENTRY_PLATFORMS = {
'switch': ['mqtt'],
'climate': ['mqtt'],
'alarm_control_panel': ['mqtt'],
+ 'fan': ['mqtt'],
}
ALREADY_DISCOVERED = 'mqtt_discovered_components'
@@ -57,6 +58,114 @@ CONFIG_ENTRY_IS_SETUP = 'mqtt_config_entry_is_setup'
MQTT_DISCOVERY_UPDATED = 'mqtt_discovery_updated_{}'
MQTT_DISCOVERY_NEW = 'mqtt_discovery_new_{}_{}'
+TOPIC_BASE = '~'
+
+ABBREVIATIONS = {
+ 'aux_cmd_t': 'aux_command_topic',
+ 'aux_stat_tpl': 'aux_state_template',
+ 'aux_stat_t': 'aux_state_topic',
+ 'avty_t': 'availability_topic',
+ 'away_mode_cmd_t': 'away_mode_command_topic',
+ 'away_mode_stat_tpl': 'away_mode_state_template',
+ 'away_mode_stat_t': 'away_mode_state_topic',
+ 'bri_cmd_t': 'brightness_command_topic',
+ 'bri_scl': 'brightness_scale',
+ 'bri_stat_t': 'brightness_state_topic',
+ 'bri_val_tpl': 'brightness_value_template',
+ 'clr_temp_cmd_t': 'color_temp_command_topic',
+ 'clr_temp_stat_t': 'color_temp_state_topic',
+ 'clr_temp_val_tpl': 'color_temp_value_template',
+ 'cmd_t': 'command_topic',
+ 'curr_temp_t': 'current_temperature_topic',
+ 'dev_cla': 'device_class',
+ 'fx_cmd_t': 'effect_command_topic',
+ 'fx_list': 'effect_list',
+ 'fx_stat_t': 'effect_state_topic',
+ 'fx_val_tpl': 'effect_value_template',
+ 'exp_aft': 'expire_after',
+ 'fan_mode_cmd_t': 'fan_mode_command_topic',
+ 'fan_mode_stat_tpl': 'fan_mode_state_template',
+ 'fan_mode_stat_t': 'fan_mode_state_topic',
+ 'frc_upd': 'force_update',
+ 'hold_cmd_t': 'hold_command_topic',
+ 'hold_stat_tpl': 'hold_state_template',
+ 'hold_stat_t': 'hold_state_topic',
+ 'ic': 'icon',
+ 'init': 'initial',
+ 'json_attr': 'json_attributes',
+ 'max_temp': 'max_temp',
+ 'min_temp': 'min_temp',
+ 'mode_cmd_t': 'mode_command_topic',
+ 'mode_stat_tpl': 'mode_state_template',
+ 'mode_stat_t': 'mode_state_topic',
+ 'name': 'name',
+ 'on_cmd_type': 'on_command_type',
+ 'opt': 'optimistic',
+ 'osc_cmd_t': 'oscillation_command_topic',
+ 'osc_stat_t': 'oscillation_state_topic',
+ 'osc_val_tpl': 'oscillation_value_template',
+ 'pl_arm_away': 'payload_arm_away',
+ 'pl_arm_home': 'payload_arm_home',
+ 'pl_avail': 'payload_available',
+ 'pl_cls': 'payload_close',
+ 'pl_disarm': 'payload_disarm',
+ 'pl_hi_spd': 'payload_high_speed',
+ 'pl_lock': 'payload_lock',
+ 'pl_lo_spd': 'payload_low_speed',
+ 'pl_med_spd': 'payload_medium_speed',
+ 'pl_not_avail': 'payload_not_available',
+ 'pl_off': 'payload_off',
+ 'pl_on': 'payload_on',
+ 'pl_open': 'payload_open',
+ 'pl_osc_off': 'payload_oscillation_off',
+ 'pl_osc_on': 'payload_oscillation_on',
+ 'pl_stop': 'payload_stop',
+ 'pl_unlk': 'payload_unlock',
+ 'pow_cmd_t': 'power_command_topic',
+ 'ret': 'retain',
+ 'rgb_cmd_tpl': 'rgb_command_template',
+ 'rgb_cmd_t': 'rgb_command_topic',
+ 'rgb_stat_t': 'rgb_state_topic',
+ 'rgb_val_tpl': 'rgb_value_template',
+ 'send_if_off': 'send_if_off',
+ 'set_pos_tpl': 'set_position_template',
+ 'set_pos_t': 'set_position_topic',
+ 'spd_cmd_t': 'speed_command_topic',
+ 'spd_stat_t': 'speed_state_topic',
+ 'spd_val_tpl': 'speed_value_template',
+ 'spds': 'speeds',
+ 'stat_clsd': 'state_closed',
+ 'stat_off': 'state_off',
+ 'stat_on': 'state_on',
+ 'stat_open': 'state_open',
+ 'stat_t': 'state_topic',
+ 'stat_val_tpl': 'state_value_template',
+ 'swing_mode_cmd_t': 'swing_mode_command_topic',
+ 'swing_mode_stat_tpl': 'swing_mode_state_template',
+ 'swing_mode_stat_t': 'swing_mode_state_topic',
+ 'temp_cmd_t': 'temperature_command_topic',
+ 'temp_stat_tpl': 'temperature_state_template',
+ 'temp_stat_t': 'temperature_state_topic',
+ 'tilt_clsd_val': 'tilt_closed_value',
+ 'tilt_cmd_t': 'tilt_command_topic',
+ 'tilt_inv_stat': 'tilt_invert_state',
+ 'tilt_max': 'tilt_max',
+ 'tilt_min': 'tilt_min',
+ 'tilt_opnd_val': 'tilt_opened_value',
+ 'tilt_status_opt': 'tilt_status_optimistic',
+ 'tilt_status_t': 'tilt_status_topic',
+ 't': 'topic',
+ 'uniq_id': 'unique_id',
+ 'unit_of_meas': 'unit_of_measurement',
+ 'val_tpl': 'value_template',
+ 'whit_val_cmd_t': 'white_value_command_topic',
+ 'whit_val_stat_t': 'white_value_state_topic',
+ 'whit_val_tpl': 'white_value_template',
+ 'xy_cmd_t': 'xy_command_topic',
+ 'xy_stat_t': 'xy_state_topic',
+ 'xy_val_tpl': 'xy_value_template',
+}
+
async def async_start(hass: HomeAssistantType, discovery_topic, hass_config,
config_entry=None) -> bool:
@@ -74,6 +183,29 @@ async def async_start(hass: HomeAssistantType, discovery_topic, hass_config,
_LOGGER.warning("Component %s is not supported", component)
return
+ if payload:
+ try:
+ payload = json.loads(payload)
+ except ValueError:
+ _LOGGER.warning("Unable to parse JSON %s: '%s'",
+ object_id, payload)
+ return
+
+ payload = dict(payload)
+
+ for key in list(payload.keys()):
+ abbreviated_key = key
+ key = ABBREVIATIONS.get(key, key)
+ payload[key] = payload.pop(abbreviated_key)
+
+ if TOPIC_BASE in payload:
+ base = payload[TOPIC_BASE]
+ for key, value in payload.items():
+ if value[0] == TOPIC_BASE and key.endswith('_topic'):
+ payload[key] = "{}{}".format(base, value[1:])
+ if value[-1] == TOPIC_BASE and key.endswith('_topic'):
+ payload[key] = "{}{}".format(value[:-1], base)
+
# If present, the node_id will be included in the discovered object id
discovery_id = '_'.join((node_id, object_id)) if node_id else object_id
@@ -90,14 +222,6 @@ async def async_start(hass: HomeAssistantType, discovery_topic, hass_config,
hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), payload)
elif payload:
# Add component
- try:
- payload = json.loads(payload)
- except ValueError:
- _LOGGER.warning("Unable to parse JSON %s: '%s'",
- object_id, payload)
- return
-
- payload = dict(payload)
platform = payload.get(CONF_PLATFORM, 'mqtt')
if platform not in ALLOWED_PLATFORMS.get(component, []):
_LOGGER.warning("Platform %s (component %s) is not allowed",
diff --git a/homeassistant/components/nest/.translations/hu.json b/homeassistant/components/nest/.translations/hu.json
index 142747a016f..aa99b46e576 100644
--- a/homeassistant/components/nest/.translations/hu.json
+++ b/homeassistant/components/nest/.translations/hu.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "already_setup": "Csak egy Nest-fi\u00f3kot konfigur\u00e1lhat.",
"authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n."
},
"error": {
@@ -18,7 +19,8 @@
"link": {
"data": {
"code": "PIN-k\u00f3d"
- }
+ },
+ "title": "Nest fi\u00f3k \u00f6sszekapcsol\u00e1sa"
}
},
"title": "Nest"
diff --git a/homeassistant/components/nest/.translations/ko.json b/homeassistant/components/nest/.translations/ko.json
index 0caa70aeff2..a53a26bca5a 100644
--- a/homeassistant/components/nest/.translations/ko.json
+++ b/homeassistant/components/nest/.translations/ko.json
@@ -4,7 +4,7 @@
"already_setup": "\ud558\ub098\uc758 Nest \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
"authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
"authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "no_flows": "Nest \ub97c \uc778\uc99d\ud558\uae30 \uc804\uc5d0 Nest \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/nest/)\ub97c \uc77d\uc5b4\ubcf4\uc138\uc694."
+ "no_flows": "Nest \ub97c \uc778\uc99d\ud558\uae30 \uc804\uc5d0 Nest \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/nest/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694."
},
"error": {
"internal_error": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \ub0b4\ubd80 \uc624\ub958 \ubc1c\uc0dd",
@@ -24,7 +24,7 @@
"data": {
"code": "\ud540 \ucf54\ub4dc"
},
- "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74, [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url})\uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4, \uc544\ub798\uc758 \ud540 \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec\ub123\uc73c\uc138\uc694.",
+ "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74, [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url}) \uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4, \uc544\ub798\uc758 \ud540 \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec\ub123\uc73c\uc138\uc694.",
"title": "Nest \uacc4\uc815 \uc5f0\uacb0"
}
},
diff --git a/homeassistant/components/nest/.translations/pt.json b/homeassistant/components/nest/.translations/pt.json
index 40743fe3ddb..5ea970d9fb3 100644
--- a/homeassistant/components/nest/.translations/pt.json
+++ b/homeassistant/components/nest/.translations/pt.json
@@ -28,6 +28,6 @@
"title": "Associar conta Nest"
}
},
- "title": ""
+ "title": "Nest"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/.translations/ro.json b/homeassistant/components/nest/.translations/ro.json
new file mode 100644
index 00000000000..f315cf549fb
--- /dev/null
+++ b/homeassistant/components/nest/.translations/ro.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "link": {
+ "data": {
+ "code": "Cod PIN"
+ },
+ "title": "Leg\u0103tur\u0103 cont Nest"
+ }
+ },
+ "title": "Nest"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/notify/aws_lambda.py b/homeassistant/components/notify/aws_lambda.py
index 8a3cb900f4b..28fedf6434d 100644
--- a/homeassistant/components/notify/aws_lambda.py
+++ b/homeassistant/components/notify/aws_lambda.py
@@ -17,7 +17,7 @@ from homeassistant.components.notify import (
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.json import JSONEncoder
-REQUIREMENTS = ['boto3==1.4.7']
+REQUIREMENTS = ['boto3==1.9.16']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/notify/aws_sns.py b/homeassistant/components/notify/aws_sns.py
index 7ecf5a7cc7f..065898bcb85 100644
--- a/homeassistant/components/notify/aws_sns.py
+++ b/homeassistant/components/notify/aws_sns.py
@@ -17,7 +17,7 @@ from homeassistant.components.notify import (
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ["boto3==1.4.7"]
+REQUIREMENTS = ["boto3==1.9.16"]
CONF_REGION = 'region_name'
CONF_ACCESS_KEY_ID = 'aws_access_key_id'
diff --git a/homeassistant/components/notify/aws_sqs.py b/homeassistant/components/notify/aws_sqs.py
index 30b673846e7..78e71bde97a 100644
--- a/homeassistant/components/notify/aws_sqs.py
+++ b/homeassistant/components/notify/aws_sqs.py
@@ -16,7 +16,7 @@ from homeassistant.components.notify import (
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ["boto3==1.4.7"]
+REQUIREMENTS = ["boto3==1.9.16"]
CONF_REGION = 'region_name'
CONF_ACCESS_KEY_ID = 'aws_access_key_id'
diff --git a/homeassistant/components/notify/clicksend.py b/homeassistant/components/notify/clicksend.py
index c028da2c579..5506d6ed6d0 100644
--- a/homeassistant/components/notify/clicksend.py
+++ b/homeassistant/components/notify/clicksend.py
@@ -22,6 +22,8 @@ _LOGGER = logging.getLogger(__name__)
BASE_API_URL = 'https://rest.clicksend.com/v3'
+DEFAULT_SENDER = 'hass'
+
HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON}
@@ -29,7 +31,7 @@ def validate_sender(config):
"""Set the optional sender name if sender name is not provided."""
if CONF_SENDER in config:
return config
- config[CONF_SENDER] = config[CONF_RECIPIENT]
+ config[CONF_SENDER] = DEFAULT_SENDER
return config
@@ -61,7 +63,7 @@ class ClicksendNotificationService(BaseNotificationService):
self.username = config.get(CONF_USERNAME)
self.api_key = config.get(CONF_API_KEY)
self.recipients = config.get(CONF_RECIPIENT)
- self.sender = config.get(CONF_SENDER, CONF_RECIPIENT)
+ self.sender = config.get(CONF_SENDER)
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
diff --git a/homeassistant/components/notify/homematic.py b/homeassistant/components/notify/homematic.py
new file mode 100644
index 00000000000..2897123c690
--- /dev/null
+++ b/homeassistant/components/notify/homematic.py
@@ -0,0 +1,61 @@
+"""
+Notification support for Homematic.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/notify.homematic/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.notify import (
+ BaseNotificationService, PLATFORM_SCHEMA, ATTR_DATA)
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.homematic import (
+ DOMAIN, SERVICE_SET_DEVICE_VALUE, ATTR_ADDRESS, ATTR_CHANNEL, ATTR_PARAM,
+ ATTR_VALUE, ATTR_INTERFACE)
+import homeassistant.helpers.template as template_helper
+
+_LOGGER = logging.getLogger(__name__)
+DEPENDENCIES = ["homematic"]
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper),
+ vol.Required(ATTR_CHANNEL): vol.Coerce(int),
+ vol.Required(ATTR_PARAM): vol.All(cv.string, vol.Upper),
+ vol.Required(ATTR_VALUE): cv.match_all,
+ vol.Optional(ATTR_INTERFACE): cv.string,
+})
+
+
+def get_service(hass, config, discovery_info=None):
+ """Get the Homematic notification service."""
+ data = {
+ ATTR_ADDRESS: config[ATTR_ADDRESS],
+ ATTR_CHANNEL: config[ATTR_CHANNEL],
+ ATTR_PARAM: config[ATTR_PARAM],
+ ATTR_VALUE: config[ATTR_VALUE]
+ }
+ if ATTR_INTERFACE in config:
+ data[ATTR_INTERFACE] = config[ATTR_INTERFACE]
+
+ return HomematicNotificationService(hass, data)
+
+
+class HomematicNotificationService(BaseNotificationService):
+ """Implement the notification service for Homematic."""
+
+ def __init__(self, hass, data):
+ """Initialize the service."""
+ self.hass = hass
+ self.data = data
+
+ def send_message(self, message="", **kwargs):
+ """Send a notification to the device."""
+ data = {**self.data, **kwargs.get(ATTR_DATA, {})}
+
+ if data.get(ATTR_VALUE) is not None:
+ templ = template_helper.Template(self.data[ATTR_VALUE], self.hass)
+ data[ATTR_VALUE] = template_helper.render_complex(templ, None)
+
+ self.hass.services.call(DOMAIN, SERVICE_SET_DEVICE_VALUE, data)
diff --git a/homeassistant/components/notify/twilio_call.py b/homeassistant/components/notify/twilio_call.py
index b517808d2ce..538a4fd9512 100644
--- a/homeassistant/components/notify/twilio_call.py
+++ b/homeassistant/components/notify/twilio_call.py
@@ -42,7 +42,7 @@ class TwilioCallNotificationService(BaseNotificationService):
def send_message(self, message="", **kwargs):
"""Call to specified target users."""
- from twilio import TwilioRestException
+ from twilio.base.exceptions import TwilioRestException
targets = kwargs.get(ATTR_TARGET)
diff --git a/homeassistant/components/notify/webostv.py b/homeassistant/components/notify/webostv.py
index 78c43c5f0ad..92762b03aea 100644
--- a/homeassistant/components/notify/webostv.py
+++ b/homeassistant/components/notify/webostv.py
@@ -13,7 +13,7 @@ from homeassistant.components.notify import (
ATTR_DATA, BaseNotificationService, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_FILENAME, CONF_HOST, CONF_ICON)
-REQUIREMENTS = ['pylgtv==0.1.7']
+REQUIREMENTS = ['pylgtv==0.1.9']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py
index c5678dff351..1f4417e07b5 100644
--- a/homeassistant/components/notify/xmpp.py
+++ b/homeassistant/components/notify/xmpp.py
@@ -12,12 +12,9 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (
ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import (
- CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT, CONF_ROOM)
+ CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT, CONF_ROOM, CONF_RESOURCE)
-REQUIREMENTS = ['sleekxmpp==1.3.2',
- 'dnspython3==1.15.0',
- 'pyasn1==0.3.7',
- 'pyasn1-modules==0.1.5']
+REQUIREMENTS = ['slixmpp==1.4.0']
_LOGGER = logging.getLogger(__name__)
@@ -31,84 +28,94 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TLS, default=True): cv.boolean,
vol.Optional(CONF_VERIFY, default=True): cv.boolean,
vol.Optional(CONF_ROOM, default=''): cv.string,
+ vol.Optional(CONF_RESOURCE, default="home-assistant"): cv.string,
})
-def get_service(hass, config, discovery_info=None):
+async def async_get_service(hass, config, discovery_info=None):
"""Get the Jabber (XMPP) notification service."""
return XmppNotificationService(
- config.get(CONF_SENDER), config.get(CONF_PASSWORD),
- config.get(CONF_RECIPIENT), config.get(CONF_TLS),
- config.get(CONF_VERIFY), config.get(CONF_ROOM))
+ config.get(CONF_SENDER), config.get(CONF_RESOURCE),
+ config.get(CONF_PASSWORD), config.get(CONF_RECIPIENT),
+ config.get(CONF_TLS), config.get(CONF_VERIFY),
+ config.get(CONF_ROOM), hass.loop)
class XmppNotificationService(BaseNotificationService):
"""Implement the notification service for Jabber (XMPP)."""
- def __init__(self, sender, password, recipient, tls, verify, room):
+ def __init__(self, sender, resource, password,
+ recipient, tls, verify, room, loop):
"""Initialize the service."""
+ self._loop = loop
self._sender = sender
+ self._resource = resource
self._password = password
self._recipient = recipient
self._tls = tls
self._verify = verify
self._room = room
- def send_message(self, message="", **kwargs):
+ async def async_send_message(self, message="", **kwargs):
"""Send a message to a user."""
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
data = '{}: {}'.format(title, message) if title else message
- send_message('{}/home-assistant'.format(self._sender),
- self._password, self._recipient, self._tls,
- self._verify, self._room, data)
+ await async_send_message(
+ '{}/{}'.format(self._sender, self._resource),
+ self._password, self._recipient, self._tls,
+ self._verify, self._room, self._loop, data)
-def send_message(sender, password, recipient, use_tls,
- verify_certificate, room, message):
+async def async_send_message(sender, password, recipient, use_tls,
+ verify_certificate, room, loop, message):
"""Send a message over XMPP."""
- import sleekxmpp
+ import slixmpp
- class SendNotificationBot(sleekxmpp.ClientXMPP):
+ class SendNotificationBot(slixmpp.ClientXMPP):
"""Service for sending Jabber (XMPP) messages."""
def __init__(self):
"""Initialize the Jabber Bot."""
- super(SendNotificationBot, self).__init__(sender, password)
+ super().__init__(sender, password)
- self.use_tls = use_tls
+ # need hass.loop!!
+ self.loop = loop
+
+ self.force_starttls = use_tls
self.use_ipv6 = False
- self.add_event_handler('failed_auth', self.check_credentials)
+ self.add_event_handler(
+ 'failed_auth', self.disconnect_on_login_fail)
self.add_event_handler('session_start', self.start)
+
if room:
self.register_plugin('xep_0045') # MUC
if not verify_certificate:
self.add_event_handler('ssl_invalid_cert',
self.discard_ssl_invalid_cert)
- self.connect(use_tls=self.use_tls, use_ssl=False)
- self.process()
+ self.connect(force_starttls=self.force_starttls, use_ssl=False)
def start(self, event):
"""Start the communication and sends the message."""
- self.send_presence()
self.get_roster()
-
+ self.send_presence()
if room:
- _LOGGER.debug("Joining room %s.", room)
- self.plugin['xep_0045'].joinMUC(room, sender, wait=True)
+ _LOGGER.debug("Joining room %s", room)
+ self.plugin['xep_0045'].join_muc(room, sender, wait=True)
self.send_message(mto=room, mbody=message, mtype='groupchat')
else:
self.send_message(mto=recipient, mbody=message, mtype='chat')
self.disconnect(wait=True)
- def check_credentials(self, event):
+ def disconnect_on_login_fail(self, event):
"""Disconnect from the server if credentials are invalid."""
+ _LOGGER.warning('Login failed')
self.disconnect()
@staticmethod
def discard_ssl_invalid_cert(event):
"""Do nothing if ssl certificate is invalid."""
- _LOGGER.info('Ignoring invalid ssl certificate as requested.')
+ _LOGGER.info('Ignoring invalid ssl certificate as requested')
SendNotificationBot()
diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py
index ff52ad94d8b..2a39ac2c44a 100644
--- a/homeassistant/components/octoprint.py
+++ b/homeassistant/components/octoprint.py
@@ -11,43 +11,117 @@ import requests
import voluptuous as vol
from aiohttp.hdrs import CONTENT_TYPE
-from homeassistant.const import CONF_API_KEY, CONF_HOST, CONTENT_TYPE_JSON
+from homeassistant.components.discovery import SERVICE_OCTOPRINT
+from homeassistant.const import (
+ CONF_API_KEY, CONF_HOST, CONTENT_TYPE_JSON, CONF_NAME, CONF_PORT,
+ CONF_SSL, TEMP_CELSIUS, CONF_MONITORED_CONDITIONS, CONF_SENSORS,
+ CONF_BINARY_SENSORS)
+from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.discovery import load_platform
+from homeassistant.util import slugify as util_slugify
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'octoprint'
CONF_NUMBER_OF_TOOLS = 'number_of_tools'
CONF_BED = 'bed'
+DEFAULT_NAME = 'OctoPrint'
+
+
+def has_all_unique_names(value):
+ """Validate that printers have an unique name."""
+ names = [util_slugify(printer['name']) for printer in value]
+ vol.Schema(vol.Unique())(names)
+ return value
+
+
+BINARY_SENSOR_TYPES = {
+ # API Endpoint, Group, Key, unit
+ 'Printing': ['printer', 'state', 'printing', None],
+ 'Printing Error': ['printer', 'state', 'error', None]
+}
+
+BINARY_SENSOR_SCHEMA = vol.Schema({
+ vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSOR_TYPES)):
+ vol.All(cv.ensure_list, [vol.In(BINARY_SENSOR_TYPES)]),
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+})
+
+SENSOR_TYPES = {
+ # API Endpoint, Group, Key, unit, icon
+ 'Temperatures': ['printer', 'temperature', '*', TEMP_CELSIUS],
+ 'Current State': ['printer', 'state', 'text', None, 'mdi:printer-3d'],
+ 'Job Percentage': ['job', 'progress', 'completion', '%',
+ 'mdi:file-percent'],
+ 'Time Remaining': ['job', 'progress', 'printTimeLeft', 'seconds',
+ 'mdi:clock-end'],
+ 'Time Elapsed': ['job', 'progress', 'printTime', 'seconds',
+ 'mdi:clock-start'],
+}
+
+SENSOR_SCHEMA = vol.Schema({
+ 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,
+})
CONFIG_SCHEMA = vol.Schema({
- DOMAIN: vol.Schema({
+ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_SSL, default=False): cv.boolean,
+ vol.Optional(CONF_PORT, default=80): cv.port,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_NUMBER_OF_TOOLS, default=0): cv.positive_int,
- vol.Optional(CONF_BED, default=False): cv.boolean
- }),
+ vol.Optional(CONF_BED, default=False): cv.boolean,
+ vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
+ vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA
+ })], has_all_unique_names),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the OctoPrint component."""
- base_url = 'http://{}/api/'.format(config[DOMAIN][CONF_HOST])
- api_key = config[DOMAIN][CONF_API_KEY]
- number_of_tools = config[DOMAIN][CONF_NUMBER_OF_TOOLS]
- bed = config[DOMAIN][CONF_BED]
+ printers = hass.data[DOMAIN] = {}
+ success = False
- hass.data[DOMAIN] = {"api": None}
+ def device_discovered(service, info):
+ """Get called when an Octoprint server has been discovered."""
+ _LOGGER.debug('Found an Octoprint server: %s', info)
- try:
- octoprint_api = OctoPrintAPI(base_url, api_key, bed, number_of_tools)
- hass.data[DOMAIN]["api"] = octoprint_api
- octoprint_api.get('printer')
- octoprint_api.get('job')
- except requests.exceptions.RequestException as conn_err:
- _LOGGER.error("Error setting up OctoPrint API: %r", conn_err)
+ discovery.listen(hass, SERVICE_OCTOPRINT, device_discovered)
- return True
+ for printer in config[DOMAIN]:
+ name = printer[CONF_NAME]
+ ssl = 's' if printer[CONF_SSL] else ''
+ base_url = 'http{}://{}:{}/api/'.format(ssl,
+ printer[CONF_HOST],
+ printer[CONF_PORT])
+ api_key = printer[CONF_API_KEY]
+ number_of_tools = printer[CONF_NUMBER_OF_TOOLS]
+ bed = printer[CONF_BED]
+ try:
+ octoprint_api = OctoPrintAPI(base_url, api_key, bed,
+ number_of_tools)
+ printers[base_url] = octoprint_api
+ octoprint_api.get('printer')
+ octoprint_api.get('job')
+ except requests.exceptions.RequestException as conn_err:
+ _LOGGER.error("Error setting up OctoPrint API: %r", conn_err)
+ continue
+
+ sensors = printer[CONF_SENSORS][CONF_MONITORED_CONDITIONS]
+ load_platform(hass, 'sensor', DOMAIN, {'name': name,
+ 'base_url': base_url,
+ 'sensors': sensors})
+ b_sensors = printer[CONF_BINARY_SENSORS][CONF_MONITORED_CONDITIONS]
+ load_platform(hass, 'binary_sensor', DOMAIN, {'name': name,
+ 'base_url': base_url,
+ 'sensors': b_sensors})
+ success = True
+
+ return success
class OctoPrintAPI:
diff --git a/homeassistant/components/opentherm_gw.py b/homeassistant/components/opentherm_gw.py
new file mode 100644
index 00000000000..08807a2d2a6
--- /dev/null
+++ b/homeassistant/components/opentherm_gw.py
@@ -0,0 +1,205 @@
+"""
+Support for OpenTherm Gateway devices.
+
+For more details about this component, please refer to the documentation at
+http://home-assistant.io/components/opentherm_gw/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.binary_sensor import DOMAIN as COMP_BINARY_SENSOR
+from homeassistant.components.sensor import DOMAIN as COMP_SENSOR
+from homeassistant.const import (CONF_DEVICE, CONF_MONITORED_VARIABLES,
+ CONF_NAME, PRECISION_HALVES, PRECISION_TENTHS,
+ PRECISION_WHOLE)
+from homeassistant.helpers.discovery import async_load_platform
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+
+import homeassistant.helpers.config_validation as cv
+
+DOMAIN = 'opentherm_gw'
+
+CONF_CLIMATE = 'climate'
+CONF_FLOOR_TEMP = 'floor_temperature'
+CONF_PRECISION = 'precision'
+
+DATA_DEVICE = 'device'
+DATA_GW_VARS = 'gw_vars'
+DATA_OPENTHERM_GW = 'opentherm_gw'
+
+SIGNAL_OPENTHERM_GW_UPDATE = 'opentherm_gw_update'
+
+CLIMATE_SCHEMA = vol.Schema({
+ 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,
+})
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_DEVICE): cv.string,
+ vol.Optional(CONF_CLIMATE, default={}): CLIMATE_SCHEMA,
+ vol.Optional(CONF_MONITORED_VARIABLES, default=[]): vol.All(
+ cv.ensure_list, [cv.string]),
+ }),
+}, extra=vol.ALLOW_EXTRA)
+
+REQUIREMENTS = ['pyotgw==0.2b1']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup(hass, config):
+ """Set up the OpenTherm Gateway component."""
+ import pyotgw
+ conf = config[DOMAIN]
+ gateway = pyotgw.pyotgw()
+ monitored_vars = conf.get(CONF_MONITORED_VARIABLES)
+ hass.data[DATA_OPENTHERM_GW] = {
+ DATA_DEVICE: gateway,
+ DATA_GW_VARS: pyotgw.vars,
+ }
+ hass.async_create_task(connect_and_subscribe(
+ hass, conf[CONF_DEVICE], gateway))
+ hass.async_create_task(async_load_platform(
+ hass, 'climate', DOMAIN, conf.get(CONF_CLIMATE)))
+ if monitored_vars:
+ hass.async_create_task(setup_monitored_vars(hass, monitored_vars))
+ return True
+
+
+async def connect_and_subscribe(hass, device_path, gateway):
+ """Connect to serial device and subscribe report handler."""
+ await gateway.connect(hass.loop, device_path)
+ _LOGGER.debug("Connected to OpenTherm Gateway at %s", device_path)
+
+ async def handle_report(status):
+ """Handle reports from the OpenTherm Gateway."""
+ _LOGGER.debug("Received report: %s", status)
+ async_dispatcher_send(hass, SIGNAL_OPENTHERM_GW_UPDATE, status)
+ gateway.subscribe(handle_report)
+
+
+async def setup_monitored_vars(hass, monitored_vars):
+ """Set up requested sensors."""
+ gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS]
+ sensor_type_map = {
+ COMP_BINARY_SENSOR: [
+ gw_vars.DATA_MASTER_CH_ENABLED,
+ gw_vars.DATA_MASTER_DHW_ENABLED,
+ gw_vars.DATA_MASTER_COOLING_ENABLED,
+ gw_vars.DATA_MASTER_OTC_ENABLED,
+ gw_vars.DATA_MASTER_CH2_ENABLED,
+ gw_vars.DATA_SLAVE_FAULT_IND,
+ gw_vars.DATA_SLAVE_CH_ACTIVE,
+ gw_vars.DATA_SLAVE_DHW_ACTIVE,
+ gw_vars.DATA_SLAVE_FLAME_ON,
+ gw_vars.DATA_SLAVE_COOLING_ACTIVE,
+ gw_vars.DATA_SLAVE_CH2_ACTIVE,
+ gw_vars.DATA_SLAVE_DIAG_IND,
+ gw_vars.DATA_SLAVE_DHW_PRESENT,
+ gw_vars.DATA_SLAVE_CONTROL_TYPE,
+ gw_vars.DATA_SLAVE_COOLING_SUPPORTED,
+ gw_vars.DATA_SLAVE_DHW_CONFIG,
+ gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP,
+ gw_vars.DATA_SLAVE_CH2_PRESENT,
+ gw_vars.DATA_SLAVE_SERVICE_REQ,
+ gw_vars.DATA_SLAVE_REMOTE_RESET,
+ gw_vars.DATA_SLAVE_LOW_WATER_PRESS,
+ gw_vars.DATA_SLAVE_GAS_FAULT,
+ gw_vars.DATA_SLAVE_AIR_PRESS_FAULT,
+ gw_vars.DATA_SLAVE_WATER_OVERTEMP,
+ gw_vars.DATA_REMOTE_TRANSFER_DHW,
+ gw_vars.DATA_REMOTE_TRANSFER_MAX_CH,
+ gw_vars.DATA_REMOTE_RW_DHW,
+ gw_vars.DATA_REMOTE_RW_MAX_CH,
+ gw_vars.DATA_ROVRD_MAN_PRIO,
+ gw_vars.DATA_ROVRD_AUTO_PRIO,
+ gw_vars.OTGW_GPIO_A_STATE,
+ gw_vars.OTGW_GPIO_B_STATE,
+ gw_vars.OTGW_IGNORE_TRANSITIONS,
+ gw_vars.OTGW_OVRD_HB,
+ ],
+ COMP_SENSOR: [
+ gw_vars.DATA_CONTROL_SETPOINT,
+ gw_vars.DATA_MASTER_MEMBERID,
+ gw_vars.DATA_SLAVE_MEMBERID,
+ gw_vars.DATA_SLAVE_OEM_FAULT,
+ gw_vars.DATA_COOLING_CONTROL,
+ gw_vars.DATA_CONTROL_SETPOINT_2,
+ gw_vars.DATA_ROOM_SETPOINT_OVRD,
+ gw_vars.DATA_SLAVE_MAX_RELATIVE_MOD,
+ gw_vars.DATA_SLAVE_MAX_CAPACITY,
+ gw_vars.DATA_SLAVE_MIN_MOD_LEVEL,
+ gw_vars.DATA_ROOM_SETPOINT,
+ gw_vars.DATA_REL_MOD_LEVEL,
+ gw_vars.DATA_CH_WATER_PRESS,
+ gw_vars.DATA_DHW_FLOW_RATE,
+ gw_vars.DATA_ROOM_SETPOINT_2,
+ gw_vars.DATA_ROOM_TEMP,
+ gw_vars.DATA_CH_WATER_TEMP,
+ gw_vars.DATA_DHW_TEMP,
+ gw_vars.DATA_OUTSIDE_TEMP,
+ gw_vars.DATA_RETURN_WATER_TEMP,
+ gw_vars.DATA_SOLAR_STORAGE_TEMP,
+ gw_vars.DATA_SOLAR_COLL_TEMP,
+ gw_vars.DATA_CH_WATER_TEMP_2,
+ gw_vars.DATA_DHW_TEMP_2,
+ gw_vars.DATA_EXHAUST_TEMP,
+ gw_vars.DATA_SLAVE_DHW_MAX_SETP,
+ gw_vars.DATA_SLAVE_DHW_MIN_SETP,
+ gw_vars.DATA_SLAVE_CH_MAX_SETP,
+ gw_vars.DATA_SLAVE_CH_MIN_SETP,
+ gw_vars.DATA_DHW_SETPOINT,
+ gw_vars.DATA_MAX_CH_SETPOINT,
+ gw_vars.DATA_OEM_DIAG,
+ gw_vars.DATA_TOTAL_BURNER_STARTS,
+ gw_vars.DATA_CH_PUMP_STARTS,
+ gw_vars.DATA_DHW_PUMP_STARTS,
+ gw_vars.DATA_DHW_BURNER_STARTS,
+ gw_vars.DATA_TOTAL_BURNER_HOURS,
+ gw_vars.DATA_CH_PUMP_HOURS,
+ gw_vars.DATA_DHW_PUMP_HOURS,
+ gw_vars.DATA_DHW_BURNER_HOURS,
+ gw_vars.DATA_MASTER_OT_VERSION,
+ gw_vars.DATA_SLAVE_OT_VERSION,
+ gw_vars.DATA_MASTER_PRODUCT_TYPE,
+ gw_vars.DATA_MASTER_PRODUCT_VERSION,
+ gw_vars.DATA_SLAVE_PRODUCT_TYPE,
+ gw_vars.DATA_SLAVE_PRODUCT_VERSION,
+ gw_vars.OTGW_MODE,
+ gw_vars.OTGW_DHW_OVRD,
+ gw_vars.OTGW_ABOUT,
+ gw_vars.OTGW_BUILD,
+ gw_vars.OTGW_CLOCKMHZ,
+ gw_vars.OTGW_LED_A,
+ gw_vars.OTGW_LED_B,
+ gw_vars.OTGW_LED_C,
+ gw_vars.OTGW_LED_D,
+ gw_vars.OTGW_LED_E,
+ gw_vars.OTGW_LED_F,
+ gw_vars.OTGW_GPIO_A,
+ gw_vars.OTGW_GPIO_B,
+ gw_vars.OTGW_SB_TEMP,
+ gw_vars.OTGW_SETP_OVRD_MODE,
+ gw_vars.OTGW_SMART_PWR,
+ gw_vars.OTGW_THRM_DETECT,
+ gw_vars.OTGW_VREF,
+ ]
+ }
+ binary_sensors = []
+ sensors = []
+ for var in monitored_vars:
+ if var in sensor_type_map[COMP_SENSOR]:
+ sensors.append(var)
+ elif var in sensor_type_map[COMP_BINARY_SENSOR]:
+ binary_sensors.append(var)
+ else:
+ _LOGGER.error("Monitored variable not supported: %s", var)
+ if binary_sensors:
+ hass.async_create_task(async_load_platform(
+ hass, COMP_BINARY_SENSOR, DOMAIN, binary_sensors))
+ if sensors:
+ await async_load_platform(hass, COMP_SENSOR, DOMAIN, sensors)
diff --git a/homeassistant/components/openuv/.translations/pt.json b/homeassistant/components/openuv/.translations/pt.json
index 36f875efc00..48283a74106 100644
--- a/homeassistant/components/openuv/.translations/pt.json
+++ b/homeassistant/components/openuv/.translations/pt.json
@@ -15,6 +15,6 @@
"title": "Preencha com as suas informa\u00e7\u00f5es"
}
},
- "title": ""
+ "title": "OpenUV"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/openuv/.translations/ro.json b/homeassistant/components/openuv/.translations/ro.json
new file mode 100644
index 00000000000..976221188d3
--- /dev/null
+++ b/homeassistant/components/openuv/.translations/ro.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Coordonatele deja \u00eenregistrate",
+ "invalid_api_key": "Cheie API invalid\u0103"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Cheie API OpenUV",
+ "elevation": "Altitudine",
+ "latitude": "Latitudine",
+ "longitude": "Longitudine"
+ },
+ "title": "Completa\u021bi informa\u021biile dvs."
+ }
+ },
+ "title": "OpenUV"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py
index 8485e1e3201..a45d9ceb0d6 100644
--- a/homeassistant/components/openuv/__init__.py
+++ b/homeassistant/components/openuv/__init__.py
@@ -14,13 +14,14 @@ from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_API_KEY, CONF_BINARY_SENSORS, CONF_ELEVATION,
CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS,
CONF_SCAN_INTERVAL, CONF_SENSORS)
+from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
from .config_flow import configured_instances
-from .const import DOMAIN
+from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
REQUIREMENTS = ['pyopenuv==1.0.4']
_LOGGER = logging.getLogger(__name__)
@@ -31,7 +32,6 @@ DATA_PROTECTION_WINDOW = 'protection_window'
DATA_UV = 'uv'
DEFAULT_ATTRIBUTION = 'Data provided by OpenUV'
-DEFAULT_SCAN_INTERVAL = timedelta(minutes=30)
NOTIFICATION_ID = 'openuv_notification'
NOTIFICATION_TITLE = 'OpenUV Component Setup'
@@ -85,18 +85,17 @@ SENSOR_SCHEMA = vol.Schema({
})
CONFIG_SCHEMA = vol.Schema({
- DOMAIN:
- vol.Schema({
- vol.Required(CONF_API_KEY): cv.string,
- vol.Optional(CONF_ELEVATION): float,
- vol.Optional(CONF_LATITUDE): cv.latitude,
- vol.Optional(CONF_LONGITUDE): cv.longitude,
- vol.Optional(CONF_BINARY_SENSORS, default={}):
- BINARY_SENSOR_SCHEMA,
- vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
- vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
- cv.time_period,
- })
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_API_KEY): cv.string,
+ vol.Optional(CONF_ELEVATION): float,
+ vol.Optional(CONF_LATITUDE): cv.latitude,
+ vol.Optional(CONF_LONGITUDE): cv.longitude,
+ vol.Optional(CONF_BINARY_SENSORS, default={}):
+ BINARY_SENSOR_SCHEMA,
+ vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
+ vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
+ cv.time_period,
+ })
}, extra=vol.ALLOW_EXTRA)
@@ -110,27 +109,26 @@ async def async_setup(hass, config):
return True
conf = config[DOMAIN]
- latitude = conf.get(CONF_LATITUDE, hass.config.latitude)
- longitude = conf.get(CONF_LONGITUDE, hass.config.longitude)
- elevation = conf.get(CONF_ELEVATION, hass.config.elevation)
+ latitude = conf.get(CONF_LATITUDE)
+ longitude = conf.get(CONF_LONGITUDE)
identifier = '{0}, {1}'.format(latitude, longitude)
+ if identifier in configured_instances(hass):
+ return True
- if identifier not in configured_instances(hass):
- hass.async_add_job(
- hass.config_entries.flow.async_init(
- DOMAIN,
- context={'source': SOURCE_IMPORT},
- data={
- CONF_API_KEY: conf[CONF_API_KEY],
- CONF_LATITUDE: latitude,
- CONF_LONGITUDE: longitude,
- CONF_ELEVATION: elevation,
- CONF_BINARY_SENSORS: conf[CONF_BINARY_SENSORS],
- CONF_SENSORS: conf[CONF_SENSORS],
- }))
-
- hass.data[DOMAIN][CONF_SCAN_INTERVAL] = conf[CONF_SCAN_INTERVAL]
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={'source': SOURCE_IMPORT},
+ data={
+ CONF_API_KEY: conf[CONF_API_KEY],
+ CONF_LATITUDE: latitude,
+ CONF_LONGITUDE: longitude,
+ CONF_ELEVATION: conf.get(CONF_ELEVATION),
+ CONF_BINARY_SENSORS: conf[CONF_BINARY_SENSORS],
+ CONF_SENSORS: conf[CONF_SENSORS],
+ CONF_SCAN_INTERVAL: conf[CONF_SCAN_INTERVAL],
+ }))
return True
@@ -145,11 +143,10 @@ async def async_setup_entry(hass, config_entry):
openuv = OpenUV(
Client(
config_entry.data[CONF_API_KEY],
- config_entry.data.get(CONF_LATITUDE, hass.config.latitude),
- config_entry.data.get(CONF_LONGITUDE, hass.config.longitude),
+ config_entry.data[CONF_LATITUDE],
+ config_entry.data[CONF_LONGITUDE],
websession,
- altitude=config_entry.data.get(
- CONF_ELEVATION, hass.config.elevation)),
+ altitude=config_entry.data[CONF_ELEVATION]),
config_entry.data.get(CONF_BINARY_SENSORS, {}).get(
CONF_MONITORED_CONDITIONS, list(BINARY_SENSORS)),
config_entry.data.get(CONF_SENSORS, {}).get(
@@ -157,20 +154,14 @@ async def async_setup_entry(hass, config_entry):
await openuv.async_update()
hass.data[DOMAIN][DATA_OPENUV_CLIENT][config_entry.entry_id] = openuv
except OpenUvError as err:
- _LOGGER.error('An error occurred: %s', str(err))
- hass.components.persistent_notification.create(
- 'Error: {0}
'
- 'You will need to restart hass after fixing.'
- ''.format(err),
- title=NOTIFICATION_TITLE,
- notification_id=NOTIFICATION_ID)
- return False
+ _LOGGER.error('Config entry failed: %s', err)
+ raise ConfigEntryNotReady
for component in ('binary_sensor', 'sensor'):
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
config_entry, component))
- async def refresh_sensors(event_time):
+ async def refresh(event_time):
"""Refresh OpenUV data."""
_LOGGER.debug('Refreshing OpenUV data')
await openuv.async_update()
@@ -178,24 +169,25 @@ async def async_setup_entry(hass, config_entry):
hass.data[DOMAIN][DATA_OPENUV_LISTENER][
config_entry.entry_id] = async_track_time_interval(
- hass, refresh_sensors,
- hass.data[DOMAIN].get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL))
+ hass,
+ refresh,
+ timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL]))
return True
async def async_unload_entry(hass, config_entry):
"""Unload an OpenUV config entry."""
- for component in ('binary_sensor', 'sensor'):
- await hass.config_entries.async_forward_entry_unload(
- config_entry, component)
-
hass.data[DOMAIN][DATA_OPENUV_CLIENT].pop(config_entry.entry_id)
remove_listener = hass.data[DOMAIN][DATA_OPENUV_LISTENER].pop(
config_entry.entry_id)
remove_listener()
+ for component in ('binary_sensor', 'sensor'):
+ await hass.config_entries.async_forward_entry_unload(
+ config_entry, component)
+
return True
diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py
index 6d7ae0f65bd..27ffe5c3985 100644
--- a/homeassistant/components/openuv/config_flow.py
+++ b/homeassistant/components/openuv/config_flow.py
@@ -1,16 +1,15 @@
"""Config flow to configure the OpenUV component."""
-from collections import OrderedDict
-
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.const import (
- CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE)
+ CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE,
+ CONF_SCAN_INTERVAL)
from homeassistant.helpers import aiohttp_client, config_validation as cv
-from .const import DOMAIN
+from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
@callback
@@ -33,6 +32,24 @@ class OpenUvFlowHandler(config_entries.ConfigFlow):
"""Initialize the config flow."""
pass
+ async def _show_form(self, errors=None):
+ """Show the form to the user."""
+ data_schema = vol.Schema({
+ vol.Required(CONF_API_KEY): str,
+ vol.Optional(CONF_LATITUDE, default=self.hass.config.latitude):
+ cv.latitude,
+ vol.Optional(CONF_LONGITUDE, default=self.hass.config.longitude):
+ cv.longitude,
+ vol.Optional(CONF_ELEVATION, default=self.hass.config.elevation):
+ vol.Coerce(float),
+ })
+
+ return self.async_show_form(
+ step_id='user',
+ data_schema=data_schema,
+ errors=errors if errors else {},
+ )
+
async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
return await self.async_step_user(import_config)
@@ -41,34 +58,31 @@ class OpenUvFlowHandler(config_entries.ConfigFlow):
"""Handle the start of the config flow."""
from pyopenuv.util import validate_api_key
- errors = {}
+ if not user_input:
+ return await self._show_form()
- if user_input is not None:
- identifier = '{0}, {1}'.format(
- user_input.get(CONF_LATITUDE, self.hass.config.latitude),
- user_input.get(CONF_LONGITUDE, self.hass.config.longitude))
+ latitude = user_input[CONF_LATITUDE]
+ longitude = user_input[CONF_LONGITUDE]
+ elevation = user_input[CONF_ELEVATION]
- if identifier in configured_instances(self.hass):
- errors['base'] = 'identifier_exists'
- else:
- websession = aiohttp_client.async_get_clientsession(self.hass)
- api_key_validation = await validate_api_key(
- user_input[CONF_API_KEY], websession)
- if api_key_validation:
- return self.async_create_entry(
- title=identifier,
- data=user_input,
- )
- errors['base'] = 'invalid_api_key'
+ identifier = '{0}, {1}'.format(latitude, longitude)
+ if identifier in configured_instances(self.hass):
+ return await self._show_form({CONF_LATITUDE: 'identifier_exists'})
- data_schema = OrderedDict()
- data_schema[vol.Required(CONF_API_KEY)] = str
- data_schema[vol.Optional(CONF_LATITUDE)] = cv.latitude
- data_schema[vol.Optional(CONF_LONGITUDE)] = cv.longitude
- data_schema[vol.Optional(CONF_ELEVATION)] = vol.Coerce(float)
+ websession = aiohttp_client.async_get_clientsession(self.hass)
+ api_key_validation = await validate_api_key(
+ user_input[CONF_API_KEY], websession)
- return self.async_show_form(
- step_id='user',
- data_schema=vol.Schema(data_schema),
- errors=errors,
- )
+ if not api_key_validation:
+ return await self._show_form({CONF_API_KEY: 'invalid_api_key'})
+
+ scan_interval = user_input.get(
+ CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
+ user_input.update({
+ CONF_LATITUDE: latitude,
+ CONF_LONGITUDE: longitude,
+ CONF_ELEVATION: elevation,
+ CONF_SCAN_INTERVAL: scan_interval.seconds,
+ })
+
+ return self.async_create_entry(title=identifier, data=user_input)
diff --git a/homeassistant/components/openuv/const.py b/homeassistant/components/openuv/const.py
index 1aa3d2abcaa..16623e45642 100644
--- a/homeassistant/components/openuv/const.py
+++ b/homeassistant/components/openuv/const.py
@@ -1,3 +1,6 @@
"""Define constants for the OpenUV component."""
+from datetime import timedelta
DOMAIN = 'openuv'
+
+DEFAULT_SCAN_INTERVAL = timedelta(minutes=30)
diff --git a/homeassistant/components/raspihats.py b/homeassistant/components/raspihats.py
index f43263bf4bf..5e834cdf7ec 100644
--- a/homeassistant/components/raspihats.py
+++ b/homeassistant/components/raspihats.py
@@ -4,7 +4,6 @@ Support for controlling raspihats boards.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/raspihats/
"""
-# pylint: disable=import-error,no-name-in-module
import logging
import threading
import time
@@ -125,7 +124,7 @@ class I2CHatsManager(threading.Thread):
with self._lock:
i2c_hat = self._i2c_hats.get(address)
if i2c_hat is None:
- # pylint: disable=import-error
+ # pylint: disable=import-error,no-name-in-module
import raspihats.i2c_hats as module
constructor = getattr(module, board)
i2c_hat = constructor(address)
@@ -143,6 +142,7 @@ class I2CHatsManager(threading.Thread):
def run(self):
"""Keep alive for I2C-HATs."""
+ # pylint: disable=import-error,no-name-in-module
from raspihats.i2c_hats import ResponseException
_LOGGER.info(log_message(self, "starting"))
@@ -205,6 +205,7 @@ class I2CHatsManager(threading.Thread):
def read_di(self, address, channel):
"""Read a value from a I2C-HAT digital input."""
+ # pylint: disable=import-error,no-name-in-module
from raspihats.i2c_hats import ResponseException
with self._lock:
@@ -217,6 +218,7 @@ class I2CHatsManager(threading.Thread):
def write_dq(self, address, channel, value):
"""Write a value to a I2C-HAT digital output."""
+ # pylint: disable=import-error,no-name-in-module
from raspihats.i2c_hats import ResponseException
with self._lock:
@@ -228,6 +230,7 @@ class I2CHatsManager(threading.Thread):
def read_dq(self, address, channel):
"""Read a value from a I2C-HAT digital output."""
+ # pylint: disable=import-error,no-name-in-module
from raspihats.i2c_hats import ResponseException
with self._lock:
diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py
index 7cd588683de..5a914fc3652 100644
--- a/homeassistant/components/remote/xiaomi_miio.py
+++ b/homeassistant/components/remote/xiaomi_miio.py
@@ -22,7 +22,7 @@ from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow
-REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41']
+REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py
index a8aeca273d6..3bb3bb7044b 100644
--- a/homeassistant/components/rflink.py
+++ b/homeassistant/components/rflink.py
@@ -6,7 +6,6 @@ https://home-assistant.io/components/rflink/
"""
import asyncio
from collections import defaultdict
-import functools as ft
import logging
import async_timeout
@@ -14,7 +13,7 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_COMMAND, CONF_HOST, CONF_PORT,
- EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN)
+ EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import CoreState, callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
@@ -68,6 +67,9 @@ DOMAIN = 'rflink'
SERVICE_SEND_COMMAND = 'send_command'
SIGNAL_AVAILABILITY = 'rflink_device_available'
+SIGNAL_HANDLE_EVENT = 'rflink_handle_event_{}'
+
+TMP_ENTITY = 'tmp.{}'
DEVICE_DEFAULTS_SCHEMA = vol.Schema({
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
@@ -118,7 +120,6 @@ async def async_setup(hass, config):
}
hass.data[DATA_ENTITY_GROUP_LOOKUP] = {
EVENT_KEY_COMMAND: defaultdict(list),
- EVENT_KEY_SENSOR: defaultdict(list),
}
# Allow platform to specify function to register new unknown devices
@@ -153,28 +154,38 @@ async def async_setup(hass, config):
return
# Lookup entities who registered this device id as device id or alias
- event_id = event.get('id', None)
+ event_id = event.get(EVENT_KEY_ID, None)
is_group_event = (event_type == EVENT_KEY_COMMAND and
event[EVENT_KEY_COMMAND] in RFLINK_GROUP_COMMANDS)
if is_group_event:
- entities = hass.data[DATA_ENTITY_GROUP_LOOKUP][event_type].get(
+ entity_ids = hass.data[DATA_ENTITY_GROUP_LOOKUP][event_type].get(
event_id, [])
else:
- entities = hass.data[DATA_ENTITY_LOOKUP][event_type][event_id]
+ entity_ids = hass.data[DATA_ENTITY_LOOKUP][event_type][event_id]
- if entities:
+ _LOGGER.debug('entity_ids: %s', entity_ids)
+ if entity_ids:
# Propagate event to every entity matching the device id
- for entity in entities:
- _LOGGER.debug('passing event to %s', entities)
- entity.handle_event(event)
- else:
- _LOGGER.debug('device_id not known, adding new device')
-
+ for entity in entity_ids:
+ _LOGGER.debug('passing event to %s', entity)
+ async_dispatcher_send(hass,
+ SIGNAL_HANDLE_EVENT.format(entity),
+ event)
+ elif not is_group_event:
# If device is not yet known, register with platform (if loaded)
if event_type in hass.data[DATA_DEVICE_REGISTER]:
- hass.async_run_job(
- hass.data[DATA_DEVICE_REGISTER][event_type], event)
+ _LOGGER.debug('device_id not known, adding new device')
+ # Add bogus event_id first to avoid race if we get another
+ # event before the device is created
+ # Any additional events recevied before the device has been
+ # created will thus be ignored.
+ hass.data[DATA_ENTITY_LOOKUP][event_type][
+ event_id].append(TMP_ENTITY.format(event_id))
+ hass.async_create_task(
+ hass.data[DATA_DEVICE_REGISTER][event_type](event))
+ else:
+ _LOGGER.debug('device_id not known and automatic add disabled')
# When connecting to tcp host instead of serial port (optional)
host = config[DOMAIN].get(CONF_HOST)
@@ -192,7 +203,7 @@ async def async_setup(hass, config):
# If HA is not stopping, initiate new connection
if hass.state != CoreState.stopping:
_LOGGER.warning('disconnected from Rflink, reconnecting')
- hass.async_add_job(connect)
+ hass.async_create_task(connect())
async def connect():
"""Set up connection and hook it into HA for reconnect/shutdown."""
@@ -242,7 +253,7 @@ async def async_setup(hass, config):
_LOGGER.info('Connected to Rflink')
- hass.async_add_job(connect)
+ hass.async_create_task(connect())
return True
@@ -253,26 +264,31 @@ class RflinkDevice(Entity):
"""
platform = None
- _state = STATE_UNKNOWN
+ _state = None
_available = True
- def __init__(self, device_id, hass, name=None, aliases=None, group=True,
- group_aliases=None, nogroup_aliases=None, fire_event=False,
+ def __init__(self, device_id, initial_event=None, name=None, aliases=None,
+ group=True, group_aliases=None, nogroup_aliases=None,
+ fire_event=False,
signal_repetitions=DEFAULT_SIGNAL_REPETITIONS):
"""Initialize the device."""
- self.hass = hass
-
# Rflink specific attributes for every component type
+ self._initial_event = initial_event
self._device_id = device_id
if name:
self._name = name
else:
self._name = device_id
+ self._aliases = aliases
+ self._group = group
+ self._group_aliases = group_aliases
+ self._nogroup_aliases = nogroup_aliases
self._should_fire_event = fire_event
self._signal_repetitions = signal_repetitions
- def handle_event(self, event):
+ @callback
+ def handle_event_callback(self, event):
"""Handle incoming event for device type."""
# Call platform specific event handler
self._handle_event(event)
@@ -283,7 +299,7 @@ class RflinkDevice(Entity):
# Put command onto bus for user to subscribe to
if self._should_fire_event and identify_event_type(
event) == EVENT_KEY_COMMAND:
- self.hass.bus.fire(EVENT_BUTTON_PRESSED, {
+ self.hass.bus.async_fire(EVENT_BUTTON_PRESSED, {
ATTR_ENTITY_ID: self.entity_id,
ATTR_STATE: event[EVENT_KEY_COMMAND],
})
@@ -314,7 +330,7 @@ class RflinkDevice(Entity):
@property
def assumed_state(self):
"""Assume device state until first device event sets state."""
- return self._state is STATE_UNKNOWN
+ return self._state is None
@property
def available(self):
@@ -322,15 +338,52 @@ class RflinkDevice(Entity):
return self._available
@callback
- def set_availability(self, availability):
+ def _availability_callback(self, availability):
"""Update availability state."""
self._available = availability
self.async_schedule_update_ha_state()
async def async_added_to_hass(self):
"""Register update callback."""
+ # Remove temporary bogus entity_id if added
+ tmp_entity = TMP_ENTITY.format(self._device_id)
+ if tmp_entity in self.hass.data[DATA_ENTITY_LOOKUP][
+ EVENT_KEY_SENSOR][self._device_id]:
+ self.hass.data[DATA_ENTITY_LOOKUP][
+ EVENT_KEY_SENSOR][self._device_id].remove(tmp_entity)
+
+ # Register id and aliases
+ self.hass.data[DATA_ENTITY_LOOKUP][
+ EVENT_KEY_COMMAND][self._device_id].append(self.entity_id)
+ if self._group:
+ self.hass.data[DATA_ENTITY_GROUP_LOOKUP][
+ EVENT_KEY_COMMAND][self._device_id].append(self.entity_id)
+ # aliases respond to both normal and group commands (allon/alloff)
+ if self._aliases:
+ for _id in self._aliases:
+ self.hass.data[DATA_ENTITY_LOOKUP][
+ EVENT_KEY_COMMAND][_id].append(self.entity_id)
+ self.hass.data[DATA_ENTITY_GROUP_LOOKUP][
+ EVENT_KEY_COMMAND][_id].append(self.entity_id)
+ # group_aliases only respond to group commands (allon/alloff)
+ if self._group_aliases:
+ for _id in self._group_aliases:
+ self.hass.data[DATA_ENTITY_GROUP_LOOKUP][
+ EVENT_KEY_COMMAND][_id].append(self.entity_id)
+ # nogroup_aliases only respond to normal commands
+ if self._nogroup_aliases:
+ for _id in self._nogroup_aliases:
+ self.hass.data[DATA_ENTITY_LOOKUP][
+ EVENT_KEY_COMMAND][_id].append(self.entity_id)
async_dispatcher_connect(self.hass, SIGNAL_AVAILABILITY,
- self.set_availability)
+ self._availability_callback)
+ async_dispatcher_connect(self.hass,
+ SIGNAL_HANDLE_EVENT.format(self.entity_id),
+ self.handle_event_callback)
+
+ # Process the initial event now that the entity is created
+ if self._initial_event:
+ self.handle_event_callback(self._initial_event)
class RflinkCommand(RflinkDevice):
@@ -388,7 +441,7 @@ class RflinkCommand(RflinkDevice):
cmd = 'on'
# if the state is unknown or false, it gets set as true
# if the state is true, it gets set as false
- self._state = self._state in [STATE_UNKNOWN, False]
+ self._state = self._state in [None, False]
# Cover options for RFlink
elif command == 'close_cover':
@@ -439,8 +492,8 @@ class RflinkCommand(RflinkDevice):
# Rflink protocol/transport handles asynchronous writing of buffer
# to serial/tcp device. Does not wait for command send
# confirmation.
- self.hass.async_add_job(ft.partial(
- self._protocol.send_command, self._device_id, cmd))
+ self.hass.async_create_task(self._protocol.send_command(
+ self._device_id, cmd))
if repetitions > 1:
self._repetition_task = self.hass.async_create_task(
diff --git a/homeassistant/components/route53.py b/homeassistant/components/route53.py
new file mode 100644
index 00000000000..f88a15b72b8
--- /dev/null
+++ b/homeassistant/components/route53.py
@@ -0,0 +1,113 @@
+"""
+Update the IP addresses of your Route53 DNS records.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/route53/
+"""
+from datetime import timedelta
+import logging
+
+import voluptuous as vol
+
+from homeassistant.const import CONF_DOMAIN, CONF_ZONE
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.event import track_time_interval
+
+REQUIREMENTS = ['boto3==1.9.16', 'ipify==1.0.0']
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_ACCESS_KEY_ID = 'aws_access_key_id'
+CONF_SECRET_ACCESS_KEY = 'aws_secret_access_key'
+CONF_RECORDS = 'records'
+
+DOMAIN = 'route53'
+
+INTERVAL = timedelta(minutes=60)
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_ACCESS_KEY_ID): cv.string,
+ vol.Required(CONF_DOMAIN): cv.string,
+ vol.Required(CONF_RECORDS): vol.All(cv.ensure_list, [cv.string]),
+ vol.Required(CONF_SECRET_ACCESS_KEY): cv.string,
+ vol.Required(CONF_ZONE): cv.string,
+ })
+}, extra=vol.ALLOW_EXTRA)
+
+
+def setup(hass, config):
+ """Set up the Route53 component."""
+ domain = config[DOMAIN][CONF_DOMAIN]
+ records = config[DOMAIN][CONF_RECORDS]
+ zone = config[DOMAIN][CONF_ZONE]
+ aws_access_key_id = config[DOMAIN][CONF_ACCESS_KEY_ID]
+ aws_secret_access_key = config[DOMAIN][CONF_SECRET_ACCESS_KEY]
+
+ def update_records_interval(now):
+ """Set up recurring update."""
+ _update_route53(
+ aws_access_key_id, aws_secret_access_key, zone, domain, records)
+
+ def update_records_service(now):
+ """Set up service for manual trigger."""
+ _update_route53(
+ aws_access_key_id, aws_secret_access_key, zone, domain, records)
+
+ track_time_interval(hass, update_records_interval, INTERVAL)
+
+ hass.services.register(DOMAIN, 'update_records', update_records_service)
+ return True
+
+
+def _update_route53(
+ aws_access_key_id, aws_secret_access_key, zone, domain, records):
+ import boto3
+ from ipify import get_ip
+ from ipify import exceptions
+
+ _LOGGER.debug("Starting update for zone %s", zone)
+
+ client = boto3.client(
+ DOMAIN,
+ aws_access_key_id=aws_access_key_id,
+ aws_secret_access_key=aws_secret_access_key,
+ )
+
+ # Get the IP Address and build an array of changes
+ try:
+ ipaddress = get_ip()
+
+ except exceptions.ConnectionError:
+ _LOGGER.warning("Unable to reach the ipify service")
+ return
+
+ except exceptions.ServiceError:
+ _LOGGER.warning("Unable to complete the ipfy request")
+ return
+
+ changes = []
+ for record in records:
+ _LOGGER.debug("Processing record: %s", record)
+
+ changes.append({
+ 'Action': 'UPSERT',
+ 'ResourceRecordSet': {
+ 'Name': '{}.{}'.format(record, domain),
+ 'Type': 'A',
+ 'TTL': 300,
+ 'ResourceRecords': [
+ {'Value': ipaddress},
+ ],
+ }
+ })
+
+ _LOGGER.debug("Submitting the following changes to Route53")
+ _LOGGER.debug(changes)
+
+ response = client.change_resource_record_sets(
+ HostedZoneId=zone, ChangeBatch={'Changes': changes})
+ _LOGGER.debug("Response is %s", response)
+
+ if response['ResponseMetadata']['HTTPStatusCode'] != 200:
+ _LOGGER.warning(response)
diff --git a/homeassistant/components/rpi_gpio.py b/homeassistant/components/rpi_gpio.py
index 824ec46d636..09521803aa9 100644
--- a/homeassistant/components/rpi_gpio.py
+++ b/homeassistant/components/rpi_gpio.py
@@ -4,7 +4,6 @@ Support for controlling GPIO pins of a Raspberry Pi.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/rpi_gpio/
"""
-# pylint: disable=import-error
import logging
from homeassistant.const import (
@@ -19,7 +18,7 @@ DOMAIN = 'rpi_gpio'
def setup(hass, config):
"""Set up the Raspberry PI GPIO component."""
- from RPi import GPIO
+ from RPi import GPIO # pylint: disable=import-error
def cleanup_gpio(event):
"""Stuff to do before stopping."""
@@ -36,32 +35,32 @@ def setup(hass, config):
def setup_output(port):
"""Set up a GPIO as output."""
- from RPi import GPIO
+ from RPi import GPIO # pylint: disable=import-error
GPIO.setup(port, GPIO.OUT)
def setup_input(port, pull_mode):
"""Set up a GPIO as input."""
- from RPi import GPIO
+ from RPi import GPIO # pylint: disable=import-error
GPIO.setup(port, GPIO.IN,
GPIO.PUD_DOWN if pull_mode == 'DOWN' else GPIO.PUD_UP)
def write_output(port, value):
"""Write a value to a GPIO."""
- from RPi import GPIO
+ from RPi import GPIO # pylint: disable=import-error
GPIO.output(port, value)
def read_input(port):
"""Read a value from a GPIO."""
- from RPi import GPIO
+ from RPi import GPIO # pylint: disable=import-error
return GPIO.input(port)
def edge_detect(port, event_callback, bounce):
"""Add detection for RISING and FALLING events."""
- from RPi import GPIO
+ from RPi import GPIO # pylint: disable=import-error
GPIO.add_event_detect(
port,
GPIO.BOTH,
diff --git a/homeassistant/components/sabnzbd.py b/homeassistant/components/sabnzbd.py
index 380867a3285..25fce22c641 100644
--- a/homeassistant/components/sabnzbd.py
+++ b/homeassistant/components/sabnzbd.py
@@ -15,11 +15,12 @@ from homeassistant.const import (
CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_SENSORS, CONF_SSL)
from homeassistant.core import callback
from homeassistant.helpers import discovery
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util.json import load_json, save_json
-REQUIREMENTS = ['pysabnzbd==1.0.1']
+REQUIREMENTS = ['pysabnzbd==1.1.0']
_LOGGER = logging.getLogger(__name__)
@@ -102,7 +103,8 @@ async def async_configure_sabnzbd(hass, config, use_ssl, name=DEFAULT_NAME,
hass.config.path(CONFIG_FILE))
api_key = conf.get(base_url, {}).get(CONF_API_KEY, '')
- sab_api = SabnzbdApi(base_url, api_key)
+ sab_api = SabnzbdApi(base_url, api_key,
+ session=async_get_clientsession(hass))
if await async_check_sabnzbd(sab_api):
async_setup_sabnzbd(hass, sab_api, config, name)
else:
@@ -188,7 +190,8 @@ def async_request_configuration(hass, config, host):
async def async_configuration_callback(data):
"""Handle configuration changes."""
api_key = data.get(CONF_API_KEY)
- sab_api = SabnzbdApi(host, api_key)
+ sab_api = SabnzbdApi(host, api_key,
+ session=async_get_clientsession(hass))
if not await async_check_sabnzbd(sab_api):
return
diff --git a/homeassistant/components/scene/elkm1.py b/homeassistant/components/scene/elkm1.py
new file mode 100644
index 00000000000..47dd17a56ae
--- /dev/null
+++ b/homeassistant/components/scene/elkm1.py
@@ -0,0 +1,31 @@
+"""
+Support for control of ElkM1 tasks ("macros").
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/scene.elkm1/
+"""
+
+
+from homeassistant.components.elkm1 import (
+ DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities)
+from homeassistant.components.scene import Scene
+
+DEPENDENCIES = [ELK_DOMAIN]
+
+
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Create the Elk-M1 scene platform."""
+ if discovery_info is None:
+ return
+ elk = hass.data[ELK_DOMAIN]['elk']
+ entities = create_elk_entities(hass, elk.tasks, 'task', ElkTask, [])
+ async_add_entities(entities, True)
+
+
+class ElkTask(ElkEntity, Scene):
+ """Elk-M1 task as scene."""
+
+ async def async_activate(self):
+ """Activate the task."""
+ self._element.activate()
diff --git a/homeassistant/components/scsgate.py b/homeassistant/components/scsgate.py
index dcea69cbb48..79bf4e217c9 100644
--- a/homeassistant/components/scsgate.py
+++ b/homeassistant/components/scsgate.py
@@ -42,11 +42,10 @@ def setup(hass, config):
device = config[DOMAIN][CONF_DEVICE]
global SCSGATE
- # pylint: disable=broad-except
try:
SCSGATE = SCSGate(device=device, logger=_LOGGER)
SCSGATE.start()
- except Exception as exception:
+ except Exception as exception: # pylint: disable=broad-except
_LOGGER.error("Cannot setup SCSGate component: %s", exception)
return False
@@ -101,10 +100,9 @@ class SCSGate:
if new_device_activated:
self._activate_next_device()
- # pylint: disable=broad-except
try:
self._devices[message.entity].process_event(message)
- except Exception as exception:
+ except Exception as exception: # pylint: disable=broad-except
msg = "Exception while processing event: {}".format(exception)
self._logger.error(msg)
else:
diff --git a/homeassistant/components/sensor/.translations/moon.ro.json b/homeassistant/components/sensor/.translations/moon.ro.json
new file mode 100644
index 00000000000..6f64e497c74
--- /dev/null
+++ b/homeassistant/components/sensor/.translations/moon.ro.json
@@ -0,0 +1,6 @@
+{
+ "state": {
+ "full_moon": "Lun\u0103 plin\u0103",
+ "new_moon": "Lun\u0103 nou\u0103"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/api_streams.py b/homeassistant/components/sensor/api_streams.py
index ac9b15754d0..1fbef2c5896 100644
--- a/homeassistant/components/sensor/api_streams.py
+++ b/homeassistant/components/sensor/api_streams.py
@@ -2,7 +2,7 @@
Entity to track connections to stream API.
For more details about this platform, please refer to the documentation at
-https://home-assistant.io/components/sensor.api_stream/
+https://home-assistant.io/components/sensor.api_streams/
"""
import logging
@@ -47,9 +47,9 @@ class StreamHandler(logging.Handler):
self.entity.schedule_update_ha_state()
-async def async_setup_platform(hass, config, async_add_entities,
- discovery_info=None):
- """Set up the API stream platform."""
+async def async_setup_platform(
+ hass, config, async_add_entities, discovery_info=None):
+ """Set up the API streams platform."""
entity = APICount()
handler = StreamHandler(entity)
@@ -68,7 +68,7 @@ async def async_setup_platform(hass, config, async_add_entities,
class APICount(Entity):
- """Entity to represent how many people are connected to stream API."""
+ """Entity to represent how many people are connected to the stream API."""
def __init__(self):
"""Initialize the API count."""
diff --git a/homeassistant/components/sensor/bh1750.py b/homeassistant/components/sensor/bh1750.py
index fb0a0116818..592a6acbe58 100644
--- a/homeassistant/components/sensor/bh1750.py
+++ b/homeassistant/components/sensor/bh1750.py
@@ -64,12 +64,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
-# pylint: disable=import-error
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the BH1750 sensor."""
- import smbus
- from i2csense.bh1750 import BH1750
+ import smbus # pylint: disable=import-error
+ from i2csense.bh1750 import BH1750 # pylint: disable=import-error
name = config.get(CONF_NAME)
bus_number = config.get(CONF_I2C_BUS)
diff --git a/homeassistant/components/sensor/bloomsky.py b/homeassistant/components/sensor/bloomsky.py
index 8926848102c..02cd456107f 100644
--- a/homeassistant/components/sensor/bloomsky.py
+++ b/homeassistant/components/sensor/bloomsky.py
@@ -63,6 +63,12 @@ class BloomSkySensor(Entity):
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):
diff --git a/homeassistant/components/sensor/bme280.py b/homeassistant/components/sensor/bme280.py
index f67dace817e..a6b773040ef 100644
--- a/homeassistant/components/sensor/bme280.py
+++ b/homeassistant/components/sensor/bme280.py
@@ -79,12 +79,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
-# pylint: disable=import-error
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the BME280 sensor."""
- import smbus
- from i2csense.bme280 import BME280
+ import smbus # pylint: disable=import-error
+ from i2csense.bme280 import BME280 # pylint: disable=import-error
SENSOR_TYPES[SENSOR_TEMP][1] = hass.config.units.temperature_unit
name = config.get(CONF_NAME)
diff --git a/homeassistant/components/sensor/bme680.py b/homeassistant/components/sensor/bme680.py
index c4e8baf6c05..cbcb7f1080e 100644
--- a/homeassistant/components/sensor/bme680.py
+++ b/homeassistant/components/sensor/bme680.py
@@ -115,15 +115,15 @@ async def async_setup_platform(hass, config, async_add_entities,
return
-# pylint: disable=import-error, no-member
def _setup_bme680(config):
"""Set up and configure the BME680 sensor."""
- from smbus import SMBus
+ from smbus import SMBus # pylint: disable=import-error
import bme680
sensor_handler = None
sensor = None
try:
+ # pylint: disable=no-member
i2c_address = config.get(CONF_I2C_ADDRESS)
bus = SMBus(config.get(CONF_I2C_BUS))
sensor = bme680.BME680(i2c_address, bus)
diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py
index 964a8a4cb16..a7ee5724d19 100644
--- a/homeassistant/components/sensor/bmw_connected_drive.py
+++ b/homeassistant/components/sensor/bmw_connected_drive.py
@@ -9,18 +9,32 @@ import logging
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
+from homeassistant.const import (CONF_UNIT_SYSTEM_IMPERIAL, VOLUME_LITERS,
+ VOLUME_GALLONS, LENGTH_KILOMETERS,
+ LENGTH_MILES)
DEPENDENCIES = ['bmw_connected_drive']
_LOGGER = logging.getLogger(__name__)
-ATTR_TO_HA = {
- 'mileage': ['mdi:speedometer', 'km'],
- 'remaining_range_total': ['mdi:ruler', 'km'],
- 'remaining_range_electric': ['mdi:ruler', 'km'],
- 'remaining_range_fuel': ['mdi:ruler', 'km'],
- 'max_range_electric': ['mdi:ruler', 'km'],
- 'remaining_fuel': ['mdi:gas-station', 'l'],
+ATTR_TO_HA_METRIC = {
+ 'mileage': ['mdi:speedometer', LENGTH_KILOMETERS],
+ 'remaining_range_total': ['mdi:ruler', LENGTH_KILOMETERS],
+ 'remaining_range_electric': ['mdi:ruler', LENGTH_KILOMETERS],
+ 'remaining_range_fuel': ['mdi:ruler', LENGTH_KILOMETERS],
+ 'max_range_electric': ['mdi:ruler', LENGTH_KILOMETERS],
+ 'remaining_fuel': ['mdi:gas-station', VOLUME_LITERS],
+ 'charging_time_remaining': ['mdi:update', 'h'],
+ 'charging_status': ['mdi:battery-charging', None]
+}
+
+ATTR_TO_HA_IMPERIAL = {
+ 'mileage': ['mdi:speedometer', LENGTH_MILES],
+ 'remaining_range_total': ['mdi:ruler', LENGTH_MILES],
+ 'remaining_range_electric': ['mdi:ruler', LENGTH_MILES],
+ 'remaining_range_fuel': ['mdi:ruler', LENGTH_MILES],
+ 'max_range_electric': ['mdi:ruler', LENGTH_MILES],
+ 'remaining_fuel': ['mdi:gas-station', VOLUME_GALLONS],
'charging_time_remaining': ['mdi:update', 'h'],
'charging_status': ['mdi:battery-charging', None]
}
@@ -28,6 +42,11 @@ ATTR_TO_HA = {
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the BMW sensors."""
+ if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
+ attribute_info = ATTR_TO_HA_IMPERIAL
+ else:
+ attribute_info = ATTR_TO_HA_METRIC
+
accounts = hass.data[BMW_DOMAIN]
_LOGGER.debug('Found BMW accounts: %s',
', '.join([a.name for a in accounts]))
@@ -36,9 +55,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for vehicle in account.account.vehicles:
for attribute_name in vehicle.drive_train_attributes:
device = BMWConnectedDriveSensor(account, vehicle,
- attribute_name)
+ attribute_name,
+ attribute_info)
devices.append(device)
- device = BMWConnectedDriveSensor(account, vehicle, 'mileage')
+ device = BMWConnectedDriveSensor(account, vehicle, 'mileage',
+ attribute_info)
devices.append(device)
add_entities(devices, True)
@@ -46,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class BMWConnectedDriveSensor(Entity):
"""Representation of a BMW vehicle sensor."""
- def __init__(self, account, vehicle, attribute: str):
+ def __init__(self, account, vehicle, attribute: str, attribute_info):
"""Constructor."""
self._vehicle = vehicle
self._account = account
@@ -54,6 +75,7 @@ class BMWConnectedDriveSensor(Entity):
self._state = None
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
+ self._attribute_info = attribute_info
@property
def should_poll(self) -> bool:
@@ -78,14 +100,14 @@ class BMWConnectedDriveSensor(Entity):
"""Icon to use in the frontend, if any."""
from bimmer_connected.state import ChargingState
vehicle_state = self._vehicle.state
- charging_state = vehicle_state.charging_status in \
- [ChargingState.CHARGING]
+ charging_state = vehicle_state.charging_status in [
+ ChargingState.CHARGING]
if self._attribute == 'charging_level_hv':
return icon_for_battery_level(
battery_level=vehicle_state.charging_level_hv,
charging=charging_state)
- icon, _ = ATTR_TO_HA.get(self._attribute, [None, None])
+ icon, _ = self._attribute_info.get(self._attribute, [None, None])
return icon
@property
@@ -100,7 +122,7 @@ class BMWConnectedDriveSensor(Entity):
@property
def unit_of_measurement(self) -> str:
"""Get the unit of measurement."""
- _, unit = ATTR_TO_HA.get(self._attribute, [None, None])
+ _, unit = self._attribute_info.get(self._attribute, [None, None])
return unit
@property
@@ -116,6 +138,16 @@ class BMWConnectedDriveSensor(Entity):
vehicle_state = self._vehicle.state
if self._attribute == 'charging_status':
self._state = getattr(vehicle_state, self._attribute).value
+ elif self.unit_of_measurement == VOLUME_GALLONS:
+ value = getattr(vehicle_state, self._attribute)
+ value_converted = self.hass.config.units.volume(value,
+ VOLUME_LITERS)
+ self._state = round(value_converted)
+ elif self.unit_of_measurement == LENGTH_MILES:
+ value = getattr(vehicle_state, self._attribute)
+ value_converted = self.hass.config.units.length(value,
+ LENGTH_KILOMETERS)
+ self._state = round(value_converted)
else:
self._state = getattr(vehicle_state, self._attribute)
diff --git a/homeassistant/components/sensor/dht.py b/homeassistant/components/sensor/dht.py
index 387a555219d..a3af5631a9c 100644
--- a/homeassistant/components/sensor/dht.py
+++ b/homeassistant/components/sensor/dht.py
@@ -53,8 +53,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the DHT sensor."""
- # pylint: disable=import-error
- import Adafruit_DHT
+ import Adafruit_DHT # pylint: disable=import-error
SENSOR_TYPES[SENSOR_TEMPERATURE][1] = hass.config.units.temperature_unit
available_sensors = {
diff --git a/homeassistant/components/sensor/dnsip.py b/homeassistant/components/sensor/dnsip.py
index 7b0d54cd934..c3ec5fd4ce2 100644
--- a/homeassistant/components/sensor/dnsip.py
+++ b/homeassistant/components/sensor/dnsip.py
@@ -87,8 +87,13 @@ class WanIpSensor(Entity):
async def async_update(self):
"""Get the current DNS IP address for hostname."""
- response = await self.resolver.query(self.hostname,
- self.querytype)
+ from aiodns.error import DNSError
+ try:
+ response = await self.resolver.query(self.hostname,
+ self.querytype)
+ except DNSError as err:
+ _LOGGER.warning("Exception while resolving host: %s", err)
+ response = None
if response:
self._state = response[0].host
else:
diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py
index d54959813f8..e3cf704d432 100644
--- a/homeassistant/components/sensor/dsmr.py
+++ b/homeassistant/components/sensor/dsmr.py
@@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['dsmr_parser==0.11']
+REQUIREMENTS = ['dsmr_parser==0.12']
CONF_DSMR_VERSION = 'dsmr_version'
CONF_RECONNECT_INTERVAL = 'reconnect_interval'
diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py
index 9e8dc33314a..ae3d498d30c 100644
--- a/homeassistant/components/sensor/eddystone_temperature.py
+++ b/homeassistant/components/sensor/eddystone_temperature.py
@@ -11,14 +11,14 @@ import logging
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- CONF_NAME, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP,
- EVENT_HOMEASSISTANT_START)
+ CONF_NAME, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
+ STATE_UNKNOWN, TEMP_CELSIUS)
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['beacontools[scan]==1.2.3', 'construct==2.9.41']
+REQUIREMENTS = ['beacontools[scan]==1.2.3', 'construct==2.9.45']
_LOGGER = logging.getLogger(__name__)
@@ -43,12 +43,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Validate configuration, create devices and start monitoring thread."""
bt_device_id = config.get("bt_device_id")
- beacons = config.get("beacons")
+ beacons = config.get(CONF_BEACONS)
devices = []
for dev_name, properties in beacons.items():
- namespace = get_from_conf(properties, "namespace", 20)
- instance = get_from_conf(properties, "instance", 12)
+ namespace = get_from_conf(properties, CONF_NAMESPACE, 20)
+ instance = get_from_conf(properties, CONF_INSTANCE, 12)
name = properties.get(CONF_NAME, dev_name)
if instance is None or namespace is None:
@@ -138,8 +138,7 @@ class Monitor:
additional_info['namespace'], additional_info['instance'],
packet.temperature)
- # pylint: disable=import-error
- from beacontools import (
+ from beacontools import ( # pylint: disable=import-error
BeaconScanner, EddystoneFilter, EddystoneTLMFrame)
device_filters = [EddystoneFilter(d.namespace, d.instance)
for d in devices]
diff --git a/homeassistant/components/sensor/elkm1.py b/homeassistant/components/sensor/elkm1.py
new file mode 100644
index 00000000000..288f968b2f7
--- /dev/null
+++ b/homeassistant/components/sensor/elkm1.py
@@ -0,0 +1,227 @@
+"""
+Support for control of ElkM1 sensors.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.elkm1/
+"""
+from homeassistant.components.elkm1 import (
+ DOMAIN as ELK_DOMAIN, create_elk_entities, ElkEntity)
+
+DEPENDENCIES = [ELK_DOMAIN]
+
+
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Create the Elk-M1 sensor platform."""
+ if discovery_info is None:
+ return
+
+ elk = hass.data[ELK_DOMAIN]['elk']
+ entities = create_elk_entities(
+ hass, elk.counters, 'counter', ElkCounter, [])
+ entities = create_elk_entities(
+ hass, elk.keypads, 'keypad', ElkKeypad, entities)
+ entities = create_elk_entities(
+ hass, [elk.panel], 'panel', ElkPanel, entities)
+ entities = create_elk_entities(
+ hass, elk.settings, 'setting', ElkSetting, entities)
+ entities = create_elk_entities(
+ hass, elk.zones, 'zone', ElkZone, entities)
+ async_add_entities(entities, True)
+
+
+def temperature_to_state(temperature, undefined_temperature):
+ """Convert temperature to a state."""
+ return temperature if temperature > undefined_temperature else None
+
+
+class ElkSensor(ElkEntity):
+ """Base representation of Elk-M1 sensor."""
+
+ def __init__(self, element, elk, elk_data):
+ """Initialize the base of all Elk sensors."""
+ super().__init__(element, elk, elk_data)
+ self._state = None
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+
+class ElkCounter(ElkSensor):
+ """Representation of an Elk-M1 Counter."""
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend."""
+ return 'mdi:numeric'
+
+ def _element_changed(self, element, changeset):
+ self._state = self._element.value
+
+
+class ElkKeypad(ElkSensor):
+ """Representation of an Elk-M1 Keypad."""
+
+ @property
+ def temperature_unit(self):
+ """Return the temperature unit."""
+ return self._temperature_unit
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self._temperature_unit
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend."""
+ return 'mdi:thermometer-lines'
+
+ @property
+ def device_state_attributes(self):
+ """Attributes of the sensor."""
+ from elkm1_lib.util import username
+
+ attrs = self.initial_attrs()
+ attrs['area'] = self._element.area + 1
+ attrs['temperature'] = self._element.temperature
+ attrs['last_user_time'] = self._element.last_user_time.isoformat()
+ attrs['last_user'] = self._element.last_user + 1
+ attrs['code'] = self._element.code
+ attrs['last_user_name'] = username(self._elk, self._element.last_user)
+ return attrs
+
+ def _element_changed(self, element, changeset):
+ self._state = temperature_to_state(self._element.temperature, -40)
+
+ async def async_added_to_hass(self):
+ """Register callback for ElkM1 changes and update entity state."""
+ await super().async_added_to_hass()
+ self.hass.data[ELK_DOMAIN]['keypads'][
+ self._element.index] = self.entity_id
+
+
+class ElkPanel(ElkSensor):
+ """Representation of an Elk-M1 Panel."""
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend."""
+ return "mdi:home"
+
+ @property
+ def device_state_attributes(self):
+ """Attributes of the sensor."""
+ attrs = self.initial_attrs()
+ attrs['system_trouble_status'] = self._element.system_trouble_status
+ return attrs
+
+ def _element_changed(self, element, changeset):
+ if self._elk.is_connected():
+ self._state = 'Paused' if self._element.remote_programming_status \
+ else 'Connected'
+ else:
+ self._state = 'Disconnected'
+
+
+class ElkSetting(ElkSensor):
+ """Representation of an Elk-M1 Setting."""
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend."""
+ return 'mdi:numeric'
+
+ def _element_changed(self, element, changeset):
+ self._state = self._element.value
+
+ @property
+ def device_state_attributes(self):
+ """Attributes of the sensor."""
+ from elkm1_lib.const import SettingFormat
+ attrs = self.initial_attrs()
+ attrs['value_format'] = SettingFormat(
+ self._element.value_format).name.lower()
+ return attrs
+
+
+class ElkZone(ElkSensor):
+ """Representation of an Elk-M1 Zone."""
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend."""
+ from elkm1_lib.const import ZoneType
+ zone_icons = {
+ ZoneType.FIRE_ALARM.value: 'fire',
+ ZoneType.FIRE_VERIFIED.value: 'fire',
+ ZoneType.FIRE_SUPERVISORY.value: 'fire',
+ ZoneType.KEYFOB.value: 'key',
+ ZoneType.NON_ALARM.value: 'alarm-off',
+ ZoneType.MEDICAL_ALARM.value: 'medical-bag',
+ ZoneType.POLICE_ALARM.value: 'alarm-light',
+ ZoneType.POLICE_NO_INDICATION.value: 'alarm-light',
+ ZoneType.KEY_MOMENTARY_ARM_DISARM.value: 'power',
+ ZoneType.KEY_MOMENTARY_ARM_AWAY.value: 'power',
+ ZoneType.KEY_MOMENTARY_ARM_STAY.value: 'power',
+ ZoneType.KEY_MOMENTARY_DISARM.value: 'power',
+ ZoneType.KEY_ON_OFF.value: 'toggle-switch',
+ ZoneType.MUTE_AUDIBLES.value: 'volume-mute',
+ ZoneType.POWER_SUPERVISORY.value: 'power-plug',
+ ZoneType.TEMPERATURE.value: 'thermometer-lines',
+ ZoneType.ANALOG_ZONE.value: 'speedometer',
+ ZoneType.PHONE_KEY.value: 'phone-classic',
+ ZoneType.INTERCOM_KEY.value: 'deskphone'
+ }
+ return 'mdi:{}'.format(
+ zone_icons.get(self._element.definition, 'alarm-bell'))
+
+ @property
+ def device_state_attributes(self):
+ """Attributes of the sensor."""
+ from elkm1_lib.const import (
+ ZoneLogicalStatus, ZonePhysicalStatus, ZoneType)
+
+ attrs = self.initial_attrs()
+ attrs['physical_status'] = ZonePhysicalStatus(
+ self._element.physical_status).name.lower()
+ attrs['logical_status'] = ZoneLogicalStatus(
+ self._element.logical_status).name.lower()
+ attrs['definition'] = ZoneType(
+ self._element.definition).name.lower()
+ attrs['area'] = self._element.area + 1
+ attrs['bypassed'] = self._element.bypassed
+ attrs['triggered_alarm'] = self._element.triggered_alarm
+ return attrs
+
+ @property
+ def temperature_unit(self):
+ """Return the temperature unit."""
+ from elkm1_lib.const import ZoneType
+ if self._element.definition == ZoneType.TEMPERATURE.value:
+ return self._temperature_unit
+ return None
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ from elkm1_lib.const import ZoneType
+ if self._element.definition == ZoneType.TEMPERATURE.value:
+ return self._temperature_unit
+ if self._element.definition == ZoneType.ANALOG_ZONE.value:
+ return 'V'
+ return None
+
+ def _element_changed(self, element, changeset):
+ from elkm1_lib.const import ZoneLogicalStatus, ZoneType
+ from elkm1_lib.util import pretty_const
+
+ if self._element.definition == ZoneType.TEMPERATURE.value:
+ self._state = temperature_to_state(self._element.temperature, -60)
+ elif self._element.definition == ZoneType.ANALOG_ZONE.value:
+ self._state = self._element.voltage
+ else:
+ self._state = pretty_const(ZoneLogicalStatus(
+ self._element.logical_status).name)
diff --git a/homeassistant/components/sensor/fastdotcom.py b/homeassistant/components/sensor/fastdotcom.py
index c6a56701f7c..761dc7c6a00 100644
--- a/homeassistant/components/sensor/fastdotcom.py
+++ b/homeassistant/components/sensor/fastdotcom.py
@@ -22,7 +22,6 @@ _LOGGER = logging.getLogger(__name__)
CONF_SECOND = 'second'
CONF_MINUTE = 'minute'
CONF_HOUR = 'hour'
-CONF_DAY = 'day'
CONF_MANUAL = 'manual'
ICON = 'mdi:speedometer'
@@ -34,8 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]),
vol.Optional(CONF_HOUR):
vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]),
- vol.Optional(CONF_DAY):
- vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(1, 31))]),
vol.Optional(CONF_MANUAL, default=False): cv.boolean,
})
@@ -109,8 +106,7 @@ class SpeedtestData:
if not config.get(CONF_MANUAL):
track_time_change(
hass, self.update, second=config.get(CONF_SECOND),
- minute=config.get(CONF_MINUTE), hour=config.get(CONF_HOUR),
- day=config.get(CONF_DAY))
+ minute=config.get(CONF_MINUTE), hour=config.get(CONF_HOUR))
def update(self, now):
"""Get the latest data from fast.com."""
diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py
index 1c6e857b92b..e93795c3668 100644
--- a/homeassistant/components/sensor/filter.py
+++ b/homeassistant/components/sensor/filter.py
@@ -63,7 +63,6 @@ FILTER_SCHEMA = vol.Schema({
default=DEFAULT_PRECISION): vol.Coerce(int),
})
-# pylint: disable=redefined-builtin
FILTER_OUTLIER_SCHEMA = FILTER_SCHEMA.extend({
vol.Required(CONF_FILTER_NAME): FILTER_NAME_OUTLIER,
vol.Optional(CONF_FILTER_WINDOW_SIZE,
@@ -348,7 +347,7 @@ class RangeFilter(Filter):
"""
def __init__(self, entity,
- lower_bound, upper_bound):
+ lower_bound=None, upper_bound=None):
"""Initialize Filter."""
super().__init__(FILTER_NAME_RANGE, entity=entity)
self._lower_bound = lower_bound
@@ -357,7 +356,8 @@ class RangeFilter(Filter):
def _filter_state(self, new_state):
"""Implement the range filter."""
- if self._upper_bound and new_state.state > self._upper_bound:
+ if (self._upper_bound is not None
+ and new_state.state > self._upper_bound):
self._stats_internal['erasures_up'] += 1
@@ -366,7 +366,8 @@ class RangeFilter(Filter):
self._entity, new_state)
new_state.state = self._upper_bound
- elif self._lower_bound and new_state.state < self._lower_bound:
+ elif (self._lower_bound is not None
+ and new_state.state < self._lower_bound):
self._stats_internal['erasures_low'] += 1
@@ -446,7 +447,8 @@ class TimeSMAFilter(Filter):
variant (enum): type of argorithm used to connect discrete values
"""
- def __init__(self, window_size, precision, entity, type):
+ def __init__(self, window_size, precision, entity,
+ type): # pylint: disable=redefined-builtin
"""Initialize Filter."""
super().__init__(FILTER_NAME_TIME_SMA, window_size, precision, entity)
self._time_window = window_size
diff --git a/homeassistant/components/sensor/fints.py b/homeassistant/components/sensor/fints.py
index 1704c13b5aa..e5dae70070b 100644
--- a/homeassistant/components/sensor/fints.py
+++ b/homeassistant/components/sensor/fints.py
@@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PIN, CONF_URL, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['fints==0.2.1']
+REQUIREMENTS = ['fints==1.0.1']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/fritzbox_callmonitor.py b/homeassistant/components/sensor/fritzbox_callmonitor.py
index c60d06da039..317416a15b8 100644
--- a/homeassistant/components/sensor/fritzbox_callmonitor.py
+++ b/homeassistant/components/sensor/fritzbox_callmonitor.py
@@ -239,8 +239,7 @@ class FritzBoxPhonebook:
self.number_dict = None
self.prefixes = prefixes or []
- # pylint: disable=import-error
- import fritzconnection as fc
+ import fritzconnection as fc # pylint: disable=import-error
# Establish a connection to the FRITZ!Box.
self.fph = fc.FritzPhonebook(
address=self.host, user=self.username, password=self.password)
diff --git a/homeassistant/components/sensor/geo_rss_events.py b/homeassistant/components/sensor/geo_rss_events.py
index 5085e113e92..60ae9730d80 100644
--- a/homeassistant/components/sensor/geo_rss_events.py
+++ b/homeassistant/components/sensor/geo_rss_events.py
@@ -16,7 +16,8 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_RADIUS, CONF_URL)
+ STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT, CONF_NAME,
+ CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_URL)
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['georss_client==0.3']
@@ -40,6 +41,8 @@ SCAN_INTERVAL = timedelta(minutes=5)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_URL): cv.string,
+ vol.Optional(CONF_LATITUDE): cv.latitude,
+ vol.Optional(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_CATEGORIES, default=[]):
@@ -51,8 +54,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the GeoRSS component."""
- home_latitude = hass.config.latitude
- home_longitude = hass.config.longitude
+ latitude = config.get(CONF_LATITUDE, hass.config.latitude)
+ longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
url = config.get(CONF_URL)
radius_in_km = config.get(CONF_RADIUS)
name = config.get(CONF_NAME)
@@ -60,18 +63,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
_LOGGER.debug("latitude=%s, longitude=%s, url=%s, radius=%s",
- home_latitude, home_longitude, url, radius_in_km)
+ latitude, longitude, url, radius_in_km)
# Create all sensors based on categories.
devices = []
if not categories:
- device = GeoRssServiceSensor((home_latitude, home_longitude), url,
+ device = GeoRssServiceSensor((latitude, longitude), url,
radius_in_km, None, name,
unit_of_measurement)
devices.append(device)
else:
for category in categories:
- device = GeoRssServiceSensor((home_latitude, home_longitude), url,
+ device = GeoRssServiceSensor((latitude, longitude), url,
radius_in_km, category, name,
unit_of_measurement)
devices.append(device)
@@ -81,7 +84,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class GeoRssServiceSensor(Entity):
"""Representation of a Sensor."""
- def __init__(self, home_coordinates, url, radius, category, service_name,
+ def __init__(self, coordinates, url, radius, category, service_name,
unit_of_measurement):
"""Initialize the sensor."""
self._category = category
@@ -90,7 +93,7 @@ class GeoRssServiceSensor(Entity):
self._state_attributes = None
self._unit_of_measurement = unit_of_measurement
from georss_client.generic_feed import GenericFeed
- self._feed = GenericFeed(home_coordinates, url, filter_radius=radius,
+ self._feed = GenericFeed(coordinates, url, filter_radius=radius,
filter_categories=None if not category
else [category])
@@ -145,3 +148,4 @@ class GeoRssServiceSensor(Entity):
# If no events were found due to an error then just set state to
# zero.
self._state = 0
+ self._state_attributes = {}
diff --git a/homeassistant/components/sensor/gitlab_ci.py b/homeassistant/components/sensor/gitlab_ci.py
index ceb5f75cace..1e55a7d6997 100644
--- a/homeassistant/components/sensor/gitlab_ci.py
+++ b/homeassistant/components/sensor/gitlab_ci.py
@@ -1,4 +1,9 @@
-"""Module for retrieving latest GitLab CI job information."""
+"""
+Sensor for retrieving latest GitLab CI job information.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.gitlab_ci/
+"""
from datetime import timedelta
import logging
@@ -11,38 +16,41 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
-CONF_GITLAB_ID = 'gitlab_id'
-CONF_ATTRIBUTION = "Information provided by https://gitlab.com/"
-
-ICON_HAPPY = 'mdi:emoticon-happy'
-ICON_SAD = 'mdi:emoticon-happy'
-ICON_OTHER = 'mdi:git'
-
-ATTR_BUILD_ID = 'build id'
-ATTR_BUILD_STATUS = 'build_status'
-ATTR_BUILD_STARTED = 'build_started'
-ATTR_BUILD_FINISHED = 'build_finished'
-ATTR_BUILD_DURATION = 'build_duration'
-ATTR_BUILD_COMMIT_ID = 'commit id'
-ATTR_BUILD_COMMIT_DATE = 'commit date'
-ATTR_BUILD_BRANCH = 'build branch'
-
-SCAN_INTERVAL = timedelta(seconds=300)
+REQUIREMENTS = ['python-gitlab==1.6.0']
_LOGGER = logging.getLogger(__name__)
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Required(CONF_TOKEN): cv.string,
- vol.Required(CONF_GITLAB_ID): cv.string,
- vol.Optional(CONF_NAME, default='GitLab CI Status'): cv.string,
- vol.Optional(CONF_URL, default='https://gitlab.com'): cv.string
-})
+ATTR_BUILD_BRANCH = 'build branch'
+ATTR_BUILD_COMMIT_DATE = 'commit date'
+ATTR_BUILD_COMMIT_ID = 'commit id'
+ATTR_BUILD_DURATION = 'build_duration'
+ATTR_BUILD_FINISHED = 'build_finished'
+ATTR_BUILD_ID = 'build id'
+ATTR_BUILD_STARTED = 'build_started'
+ATTR_BUILD_STATUS = 'build_status'
-REQUIREMENTS = ['python-gitlab==1.6.0']
+CONF_ATTRIBUTION = "Information provided by https://gitlab.com/"
+CONF_GITLAB_ID = 'gitlab_id'
+
+DEFAULT_NAME = 'GitLab CI Status'
+DEFAULT_URL = 'https://gitlab.com'
+
+ICON_HAPPY = 'mdi:emoticon-happy'
+ICON_OTHER = 'mdi:git'
+ICON_SAD = 'mdi:emoticon-happy'
+
+SCAN_INTERVAL = timedelta(seconds=300)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_GITLAB_ID): cv.string,
+ vol.Required(CONF_TOKEN): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_URL, default=DEFAULT_URL): cv.string
+})
def setup_platform(hass, config, add_entities, discovery_info=None):
- """Sensor platform setup."""
+ """Set up the GitLab sensor platform."""
_name = config.get(CONF_NAME)
_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
_url = config.get(CONF_URL)
@@ -58,10 +66,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class GitLabSensor(Entity):
- """Representation of a Sensor."""
+ """Representation of a GitLab sensor."""
def __init__(self, gitlab_data, name):
- """Initialize the sensor."""
+ """Initialize the GitLab sensor."""
self._available = False
self._state = None
self._started_at = None
diff --git a/homeassistant/components/sensor/haveibeenpwned.py b/homeassistant/components/sensor/haveibeenpwned.py
index 9428eaea00e..4d651ea81c7 100644
--- a/homeassistant/components/sensor/haveibeenpwned.py
+++ b/homeassistant/components/sensor/haveibeenpwned.py
@@ -42,24 +42,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
devices = []
for email in emails:
- devices.append(HaveIBeenPwnedSensor(data, hass, email))
+ devices.append(HaveIBeenPwnedSensor(data, email))
add_entities(devices)
- # To make sure we get initial data for the sensors ignoring the normal
- # throttle of 15 minutes but using an update throttle of 5 seconds
- for sensor in devices:
- sensor.update_nothrottle()
-
class HaveIBeenPwnedSensor(Entity):
"""Implementation of a HaveIBeenPwned sensor."""
- def __init__(self, data, hass, email):
+ def __init__(self, data, email):
"""Initialize the HaveIBeenPwned sensor."""
self._state = None
self._data = data
- self._hass = hass
self._email = email
self._unit_of_measurement = "Breaches"
@@ -95,6 +89,12 @@ class HaveIBeenPwnedSensor(Entity):
return val
+ async def async_added_to_hass(self):
+ """Get initial data."""
+ # To make sure we get initial data for the sensors ignoring the normal
+ # throttle of 15 minutes but using an update throttle of 5 seconds
+ self.hass.async_add_executor_job(self.update_nothrottle)
+
def update_nothrottle(self, dummy=None):
"""Update sensor without throttle."""
self._data.update_no_throttle()
@@ -106,13 +106,12 @@ class HaveIBeenPwnedSensor(Entity):
# normal using update
if self._email not in self._data.data:
track_point_in_time(
- self._hass, self.update_nothrottle,
+ self.hass, self.update_nothrottle,
dt_util.now() + MIN_TIME_BETWEEN_FORCED_UPDATES)
return
- if self._email in self._data.data:
- self._state = len(self._data.data[self._email])
- self.schedule_update_ha_state()
+ self._state = len(self._data.data[self._email])
+ self.schedule_update_ha_state()
def update(self):
"""Update data and see if it contains data for our email."""
diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py
index 8495286c143..26fa76d94a9 100644
--- a/homeassistant/components/sensor/homematic.py
+++ b/homeassistant/components/sensor/homematic.py
@@ -48,6 +48,10 @@ HM_UNIT_HA_CAST = {
'GAS_POWER': 'm3',
'GAS_ENERGY_COUNTER': 'm3',
'LUX': 'lx',
+ 'CURRENT_ILLUMINATION': 'lx',
+ 'AVERAGE_ILLUMINATION': 'lx',
+ 'LOWEST_ILLUMINATION': 'lx',
+ 'HIGHEST_ILLUMINATION': 'lx',
'RAIN_COUNTER': 'mm',
'WIND_SPEED': 'km/h',
'WIND_DIRECTION': '°',
@@ -64,6 +68,10 @@ HM_ICON_HA_CAST = {
'TEMPERATURE': 'mdi:thermometer',
'ACTUAL_TEMPERATURE': 'mdi:thermometer',
'LUX': 'mdi:weather-sunny',
+ 'CURRENT_ILLUMINATION': 'mdi:weather-sunny',
+ 'AVERAGE_ILLUMINATION': 'mdi:weather-sunny',
+ 'LOWEST_ILLUMINATION': 'mdi:weather-sunny',
+ 'HIGHEST_ILLUMINATION': 'mdi:weather-sunny',
'BRIGHTNESS': 'mdi:invert-colors',
'POWER': 'mdi:flash-red-eye',
'CURRENT': 'mdi:flash-red-eye',
diff --git a/homeassistant/components/sensor/htu21d.py b/homeassistant/components/sensor/htu21d.py
index ae2555f57f9..4f8665b2011 100644
--- a/homeassistant/components/sensor/htu21d.py
+++ b/homeassistant/components/sensor/htu21d.py
@@ -38,12 +38,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
-# pylint: disable=import-error
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the HTU21D sensor."""
- import smbus
- from i2csense.htu21d import HTU21D
+ import smbus # pylint: disable=import-error
+ from i2csense.htu21d import HTU21D # pylint: disable=import-error
name = config.get(CONF_NAME)
bus_number = config.get(CONF_I2C_BUS)
diff --git a/homeassistant/components/sensor/huawei_lte.py b/homeassistant/components/sensor/huawei_lte.py
index f5a21999ab8..5ff5f9b38ae 100644
--- a/homeassistant/components/sensor/huawei_lte.py
+++ b/homeassistant/components/sensor/huawei_lte.py
@@ -24,11 +24,14 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['huawei_lte']
-DEFAULT_NAME_TEMPLATE = 'Huawei {}: {}'
+DEFAULT_NAME_TEMPLATE = 'Huawei {} {}'
DEFAULT_SENSORS = [
"device_information.WanIPAddress",
+ "device_signal.rsrq",
+ "device_signal.rsrp",
"device_signal.rssi",
+ "device_signal.sinr",
]
SENSOR_META = {
@@ -43,6 +46,26 @@ SENSOR_META = {
name="WAN IPv6 address",
icon="mdi:ip",
),
+ "device_signal.band": dict(
+ name="Band",
+ ),
+ "device_signal.cell_id": dict(
+ name="Cell ID",
+ ),
+ "device_signal.lac": dict(
+ name="LAC",
+ ),
+ "device_signal.mode": dict(
+ name="Mode",
+ formatter=lambda x: ({
+ '0': '2G',
+ '2': '3G',
+ '7': '4G',
+ }.get(x, 'Unknown'), None),
+ ),
+ "device_signal.pci": dict(
+ name="PCI",
+ ),
"device_signal.rsrq": dict(
name="RSRQ",
# http://www.lte-anbieter.info/technik/rsrq.php
@@ -99,6 +122,22 @@ def setup_platform(
add_entities(sensors, True)
+def format_default(value):
+ """Format value."""
+ unit = None
+ if value is not None:
+ # Clean up value and infer unit, e.g. -71dBm, 15 dB
+ match = re.match(
+ r"(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value))
+ if match:
+ try:
+ value = float(match.group("value"))
+ unit = match.group("unit")
+ except ValueError:
+ pass
+ return value, unit
+
+
@attr.s
class HuaweiLteSensor(Entity):
"""Huawei LTE sensor entity."""
@@ -114,8 +153,8 @@ class HuaweiLteSensor(Entity):
def unique_id(self) -> str:
"""Return unique ID for sensor."""
return "{}_{}".format(
- self.path,
self.data["device_information.SerialNumber"],
+ ".".join(self.path),
)
@property
@@ -147,23 +186,14 @@ class HuaweiLteSensor(Entity):
"""Update state."""
self.data.update()
- unit = None
try:
value = self.data[self.path]
except KeyError:
_LOGGER.warning("%s not in data", self.path)
value = None
- if value is not None:
- # Clean up value and infer unit, e.g. -71dBm, 15 dB
- match = re.match(
- r"(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value))
- if match:
- try:
- value = float(match.group("value"))
- unit = match.group("unit")
- except ValueError:
- pass
+ formatter = self.meta.get("formatter")
+ if not callable(formatter):
+ formatter = format_default
- self._state = value
- self._unit = unit
+ self._state, self._unit = formatter(value)
diff --git a/homeassistant/components/sensor/jewish_calendar.py b/homeassistant/components/sensor/jewish_calendar.py
index 1de45d6145e..e1225b8f25d 100644
--- a/homeassistant/components/sensor/jewish_calendar.py
+++ b/homeassistant/components/sensor/jewish_calendar.py
@@ -4,6 +4,7 @@ Platform to retrieve Jewish calendar information for Home Assistant.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.jewish_calendar/
"""
+from datetime import timedelta
import logging
import voluptuous as vol
@@ -14,7 +15,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['hdate==0.6.3']
+REQUIREMENTS = ['hdate==0.6.5']
_LOGGER = logging.getLogger(__name__)
@@ -107,16 +108,20 @@ class JewishCalSensor(Entity):
import hdate
today = dt_util.now().date()
+ upcoming_saturday = today + timedelta((12 - today.weekday()) % 7)
date = hdate.HDate(
today, diaspora=self.diaspora, hebrew=self._hebrew)
+ upcoming_shabbat = hdate.HDate(
+ upcoming_saturday, diaspora=self.diaspora, hebrew=self._hebrew)
if self.type == 'date':
self._state = hdate.date.get_hebrew_date(
date.h_day, date.h_month, date.h_year, hebrew=self._hebrew)
elif self.type == 'weekly_portion':
self._state = hdate.date.get_parashe(
- date.get_reading(self.diaspora), hebrew=self._hebrew)
+ upcoming_shabbat.get_reading(self.diaspora),
+ hebrew=self._hebrew)
elif self.type == 'holiday_name':
try:
description = next(
diff --git a/homeassistant/components/sensor/knx.py b/homeassistant/components/sensor/knx.py
index ec506189c12..c096e15192d 100644
--- a/homeassistant/components/sensor/knx.py
+++ b/homeassistant/components/sensor/knx.py
@@ -42,7 +42,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(KNXSensor(hass, device))
+ entities.append(KNXSensor(device))
async_add_entities(entities)
@@ -56,17 +56,15 @@ def async_add_entities_config(hass, config, async_add_entities):
group_address=config.get(CONF_ADDRESS),
value_type=config.get(CONF_TYPE))
hass.data[DATA_KNX].xknx.devices.add(sensor)
- async_add_entities([KNXSensor(hass, sensor)])
+ async_add_entities([KNXSensor(sensor)])
class KNXSensor(Entity):
"""Representation of a KNX sensor."""
- def __init__(self, hass, device):
+ def __init__(self, device):
"""Initialize of a KNX sensor."""
self.device = device
- self.hass = hass
- self.async_register_callbacks()
@callback
def async_register_callbacks(self):
@@ -76,6 +74,10 @@ class KNXSensor(Entity):
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."""
diff --git a/homeassistant/components/sensor/linky.py b/homeassistant/components/sensor/linky.py
index 83a6d793085..316da010ae4 100644
--- a/homeassistant/components/sensor/linky.py
+++ b/homeassistant/components/sensor/linky.py
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['pylinky==0.1.6']
+REQUIREMENTS = ['pylinky==0.1.8']
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=10)
@@ -37,11 +37,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
from pylinky.client import LinkyClient, PyLinkyError
client = LinkyClient(username, password, None, timeout)
-
try:
client.fetch_data()
except PyLinkyError as exp:
_LOGGER.error(exp)
+ client.close_session()
return
devices = [LinkySensor('Linky', client)]
@@ -80,6 +80,7 @@ class LinkySensor(Entity):
self._client.fetch_data()
except PyLinkyError as exp:
_LOGGER.error(exp)
+ self._client.close_session()
return
_LOGGER.debug(json.dumps(self._client.get_data(), indent=2))
diff --git a/homeassistant/components/sensor/mold_indicator.py b/homeassistant/components/sensor/mold_indicator.py
index e5794ab1314..2a250f0e63d 100644
--- a/homeassistant/components/sensor/mold_indicator.py
+++ b/homeassistant/components/sensor/mold_indicator.py
@@ -9,12 +9,15 @@ import math
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant import util
-from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.event import track_state_change
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.core import callback
from homeassistant.const import (
- ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME)
+ ATTR_UNIT_OF_MEASUREMENT, EVENT_HOMEASSISTANT_START, STATE_UNKNOWN,
+ TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME)
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.event import async_track_state_change
+
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -41,7 +44,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
"""Set up MoldIndicator sensor."""
name = config.get(CONF_NAME, DEFAULT_NAME)
indoor_temp_sensor = config.get(CONF_INDOOR_TEMP)
@@ -49,16 +53,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
indoor_humidity_sensor = config.get(CONF_INDOOR_HUMIDITY)
calib_factor = config.get(CONF_CALIBRATION_FACTOR)
- add_entities([MoldIndicator(
- hass, name, indoor_temp_sensor, outdoor_temp_sensor,
- indoor_humidity_sensor, calib_factor)], True)
+ async_add_entities([MoldIndicator(
+ name, hass.config.units.is_metric, indoor_temp_sensor,
+ outdoor_temp_sensor, indoor_humidity_sensor, calib_factor)], False)
class MoldIndicator(Entity):
"""Represents a MoldIndication sensor."""
- def __init__(self, hass, name, indoor_temp_sensor, outdoor_temp_sensor,
- indoor_humidity_sensor, calib_factor):
+ def __init__(self, name, is_metric, indoor_temp_sensor,
+ outdoor_temp_sensor, indoor_humidity_sensor, calib_factor):
"""Initialize the sensor."""
self._state = None
self._name = name
@@ -66,7 +70,11 @@ class MoldIndicator(Entity):
self._indoor_humidity_sensor = indoor_humidity_sensor
self._outdoor_temp_sensor = outdoor_temp_sensor
self._calib_factor = calib_factor
- self._is_metric = hass.config.units.is_metric
+ self._is_metric = is_metric
+ self._available = False
+ self._entities = set([self._indoor_temp_sensor,
+ self._indoor_humidity_sensor,
+ self._outdoor_temp_sensor])
self._dewpoint = None
self._indoor_temp = None
@@ -74,34 +82,85 @@ class MoldIndicator(Entity):
self._indoor_hum = None
self._crit_temp = None
- track_state_change(hass, indoor_temp_sensor, self._sensor_changed)
- track_state_change(hass, outdoor_temp_sensor, self._sensor_changed)
- track_state_change(hass, indoor_humidity_sensor, self._sensor_changed)
+ async def async_added_to_hass(self):
+ """Register callbacks."""
+ @callback
+ def mold_indicator_sensors_state_listener(entity, old_state,
+ new_state):
+ """Handle for state changes for dependent sensors."""
+ _LOGGER.debug("Sensor state change for %s that had old state %s "
+ "and new state %s", entity, old_state, new_state)
- # Read initial state
- indoor_temp = hass.states.get(indoor_temp_sensor)
- outdoor_temp = hass.states.get(outdoor_temp_sensor)
- indoor_hum = hass.states.get(indoor_humidity_sensor)
+ if self._update_sensor(entity, old_state, new_state):
+ self.async_schedule_update_ha_state(True)
- if indoor_temp:
- self._indoor_temp = MoldIndicator._update_temp_sensor(indoor_temp)
+ @callback
+ def mold_indicator_startup(event):
+ """Add listeners and get 1st state."""
+ _LOGGER.debug("Startup for %s", self.entity_id)
- if outdoor_temp:
- self._outdoor_temp = MoldIndicator._update_temp_sensor(
- outdoor_temp)
+ async_track_state_change(self.hass, self._entities,
+ mold_indicator_sensors_state_listener)
- if indoor_hum:
- self._indoor_hum = MoldIndicator._update_hum_sensor(indoor_hum)
+ # Read initial state
+ indoor_temp = self.hass.states.get(self._indoor_temp_sensor)
+ outdoor_temp = self.hass.states.get(self._outdoor_temp_sensor)
+ indoor_hum = self.hass.states.get(self._indoor_humidity_sensor)
+
+ schedule_update = self._update_sensor(self._indoor_temp_sensor,
+ None, indoor_temp)
+
+ schedule_update = False if not self._update_sensor(
+ self._outdoor_temp_sensor, None, outdoor_temp) else\
+ schedule_update
+
+ schedule_update = False if not self._update_sensor(
+ self._indoor_humidity_sensor, None, indoor_hum) else\
+ schedule_update
+
+ if schedule_update:
+ self.async_schedule_update_ha_state(True)
+
+ self.hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_START, mold_indicator_startup)
+
+ def _update_sensor(self, entity, old_state, new_state):
+ """Update information based on new sensor states."""
+ _LOGGER.debug("Sensor update for %s", entity)
+ if new_state is None:
+ return False
+
+ # If old_state is not set and new state is unknown then it means
+ # that the sensor just started up
+ if old_state is None and new_state.state == STATE_UNKNOWN:
+ return False
+
+ if entity == self._indoor_temp_sensor:
+ self._indoor_temp = MoldIndicator._update_temp_sensor(new_state)
+ elif entity == self._outdoor_temp_sensor:
+ self._outdoor_temp = MoldIndicator._update_temp_sensor(new_state)
+ elif entity == self._indoor_humidity_sensor:
+ self._indoor_hum = MoldIndicator._update_hum_sensor(new_state)
+
+ return True
@staticmethod
def _update_temp_sensor(state):
"""Parse temperature sensor value."""
+ _LOGGER.debug("Updating temp sensor with value %s", state.state)
+
+ # Return an error if the sensor change its state to Unknown.
+ if state.state == STATE_UNKNOWN:
+ _LOGGER.error("Unable to parse temperature sensor %s with state:"
+ " %s", state.entity_id, state.state)
+ return None
+
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
temp = util.convert(state.state, float)
if temp is None:
- _LOGGER.error('Unable to parse sensor temperature: %s',
- state.state)
+ _LOGGER.error("Unable to parse temperature sensor %s with state:"
+ " %s", state.entity_id, state.state)
return None
# convert to celsius if necessary
@@ -109,56 +168,62 @@ class MoldIndicator(Entity):
return util.temperature.fahrenheit_to_celsius(temp)
if unit == TEMP_CELSIUS:
return temp
- _LOGGER.error("Temp sensor has unsupported unit: %s (allowed: %s, "
- "%s)", unit, TEMP_CELSIUS, TEMP_FAHRENHEIT)
+ _LOGGER.error("Temp sensor %s has unsupported unit: %s (allowed: %s, "
+ "%s)", state.entity_id, unit, TEMP_CELSIUS,
+ TEMP_FAHRENHEIT)
return None
@staticmethod
def _update_hum_sensor(state):
"""Parse humidity sensor value."""
+ _LOGGER.debug("Updating humidity sensor with value %s", state.state)
+
+ # Return an error if the sensor change its state to Unknown.
+ if state.state == STATE_UNKNOWN:
+ _LOGGER.error('Unable to parse humidity sensor %s, state: %s',
+ state.entity_id, state.state)
+ return None
+
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
hum = util.convert(state.state, float)
if hum is None:
- _LOGGER.error('Unable to parse sensor humidity: %s',
- state.state)
+ _LOGGER.error("Unable to parse humidity sensor %s, state: %s",
+ state.entity_id, state.state)
return None
if unit != '%':
- _LOGGER.error("Humidity sensor has unsupported unit: %s %s",
- unit, " (allowed: %)")
+ _LOGGER.error("Humidity sensor %s has unsupported unit: %s %s",
+ state.entity_id, unit, " (allowed: %)")
+ return None
if hum > 100 or hum < 0:
- _LOGGER.error("Humidity sensor out of range: %s %s", hum,
- " (allowed: 0-100%)")
+ _LOGGER.error("Humidity sensor %s is out of range: %s %s",
+ state.entity_id, hum, "(allowed: 0-100%)")
+ return None
return hum
- def update(self):
+ async def async_update(self):
"""Calculate latest state."""
+ _LOGGER.debug("Update state for %s", self.entity_id)
# check all sensors
if None in (self._indoor_temp, self._indoor_hum, self._outdoor_temp):
+ self._available = False
+ self._dewpoint = None
+ self._crit_temp = None
return
# re-calculate dewpoint and mold indicator
self._calc_dewpoint()
self._calc_moldindicator()
-
- def _sensor_changed(self, entity_id, old_state, new_state):
- """Handle sensor state changes."""
- if new_state is None:
- return
-
- if entity_id == self._indoor_temp_sensor:
- self._indoor_temp = MoldIndicator._update_temp_sensor(new_state)
- elif entity_id == self._outdoor_temp_sensor:
- self._outdoor_temp = MoldIndicator._update_temp_sensor(new_state)
- elif entity_id == self._indoor_humidity_sensor:
- self._indoor_hum = MoldIndicator._update_hum_sensor(new_state)
-
- self.update()
- self.schedule_update_ha_state()
+ if self._state is None:
+ self._available = False
+ self._dewpoint = None
+ self._crit_temp = None
+ else:
+ self._available = True
def _calc_dewpoint(self):
"""Calculate the dewpoint for the indoor air."""
@@ -183,6 +248,8 @@ class MoldIndicator(Entity):
" calibration-factor: %s",
self._dewpoint, self._calib_factor)
self._state = None
+ self._available = False
+ self._crit_temp = None
return
# first calculate the approximate temperature at the calibration point
@@ -232,6 +299,11 @@ class MoldIndicator(Entity):
"""Return the state of the entity."""
return self._state
+ @property
+ def available(self):
+ """Return the availability of this sensor."""
+ return self._available
+
@property
def device_state_attributes(self):
"""Return the state attributes."""
@@ -240,9 +312,16 @@ class MoldIndicator(Entity):
ATTR_DEWPOINT: self._dewpoint,
ATTR_CRITICAL_TEMP: self._crit_temp,
}
+
+ dewpoint = util.temperature.celsius_to_fahrenheit(self._dewpoint) \
+ if self._dewpoint is not None else None
+
+ crit_temp = util.temperature.celsius_to_fahrenheit(self._crit_temp) \
+ if self._crit_temp is not None else None
+
return {
ATTR_DEWPOINT:
- util.temperature.celsius_to_fahrenheit(self._dewpoint),
+ dewpoint,
ATTR_CRITICAL_TEMP:
- util.temperature.celsius_to_fahrenheit(self._crit_temp),
+ crit_temp,
}
diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py
index fe0b77b2024..225ed07a622 100644
--- a/homeassistant/components/sensor/mqtt.py
+++ b/homeassistant/components/sensor/mqtt.py
@@ -16,12 +16,12 @@ from homeassistant.components import sensor
from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
- MqttAvailability, MqttDiscoveryUpdate)
+ MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA
from homeassistant.const import (
CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN,
- CONF_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_DEVICE_CLASS)
+ CONF_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_DEVICE_CLASS, CONF_DEVICE)
from homeassistant.helpers.entity import Entity
from homeassistant.components import mqtt
import homeassistant.helpers.config_validation as cv
@@ -51,6 +51,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
# 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)
@@ -95,22 +96,25 @@ async def _async_setup_entity(hass: HomeAssistantType, config: ConfigType,
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
+ config.get(CONF_DEVICE),
discovery_hash,
)])
-class MqttSensor(MqttAvailability, MqttDiscoveryUpdate, Entity):
+class MqttSensor(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
+ Entity):
"""Representation of a sensor that can be updated using MQTT."""
def __init__(self, name, state_topic, qos, unit_of_measurement,
force_update, expire_after, icon, device_class: Optional[str],
value_template, json_attributes, unique_id: Optional[str],
availability_topic, payload_available, payload_not_available,
- discovery_hash):
+ device_config: Optional[ConfigType], discovery_hash):
"""Initialize the sensor."""
MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
+ MqttEntityDeviceInfo.__init__(self, device_config)
self._state = STATE_UNKNOWN
self._name = name
self._state_topic = state_topic
diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py
index d42828c9f55..8170b97c4c8 100644
--- a/homeassistant/components/sensor/octoprint.py
+++ b/homeassistant/components/sensor/octoprint.py
@@ -7,42 +7,28 @@ https://home-assistant.io/components/sensor.octoprint/
import logging
import requests
-import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (
- TEMP_CELSIUS, CONF_NAME, CONF_MONITORED_CONDITIONS)
+from homeassistant.components.octoprint import (SENSOR_TYPES,
+ DOMAIN as COMPONENT_DOMAIN)
+from homeassistant.const import (TEMP_CELSIUS)
from homeassistant.helpers.entity import Entity
-import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['octoprint']
-DOMAIN = "octoprint"
-DEFAULT_NAME = 'OctoPrint'
NOTIFICATION_ID = 'octoprint_notification'
NOTIFICATION_TITLE = 'OctoPrint sensor setup error'
-SENSOR_TYPES = {
- 'Temperatures': ['printer', 'temperature', '*', TEMP_CELSIUS],
- 'Current State': ['printer', 'state', 'text', None],
- 'Job Percentage': ['job', 'progress', 'completion', '%'],
- 'Time Remaining': ['job', 'progress', 'printTimeLeft', 'seconds'],
- 'Time Elapsed': ['job', 'progress', 'printTime', 'seconds'],
-}
-
-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 sensors."""
- octoprint_api = hass.data[DOMAIN]["api"]
- name = config.get(CONF_NAME)
- monitored_conditions = config.get(CONF_MONITORED_CONDITIONS)
+ 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]
tools = octoprint_api.get_tools()
if "Temperatures" in monitored_conditions:
@@ -72,7 +58,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
new_sensor = OctoPrintSensor(
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])
+ SENSOR_TYPES[octo_type][1], None, SENSOR_TYPES[octo_type][4])
devices.append(new_sensor)
add_entities(devices, True)
@@ -81,7 +67,7 @@ class OctoPrintSensor(Entity):
"""Representation of an OctoPrint sensor."""
def __init__(self, api, condition, sensor_type, sensor_name, unit,
- endpoint, group, tool=None):
+ endpoint, group, tool=None, icon=None):
"""Initialize a new OctoPrint sensor."""
self.sensor_name = sensor_name
if tool is None:
@@ -96,6 +82,7 @@ class OctoPrintSensor(Entity):
self.api_endpoint = endpoint
self.api_group = group
self.api_tool = tool
+ self._icon = icon
_LOGGER.debug("Created OctoPrint sensor %r", self)
@property
@@ -128,3 +115,8 @@ class OctoPrintSensor(Entity):
except requests.exceptions.ConnectionError:
# Error calling the api, already logged in api.update()
return
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend."""
+ return self._icon
diff --git a/homeassistant/components/sensor/opentherm_gw.py b/homeassistant/components/sensor/opentherm_gw.py
new file mode 100644
index 00000000000..9ae557654ce
--- /dev/null
+++ b/homeassistant/components/sensor/opentherm_gw.py
@@ -0,0 +1,211 @@
+"""
+Support for OpenTherm Gateway sensors.
+
+For more details about this platform, please refer to the documentation at
+http://home-assistant.io/components/sensor.opentherm_gw/
+"""
+import logging
+
+from homeassistant.components.opentherm_gw import (
+ DATA_GW_VARS, DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE)
+from homeassistant.components.sensor import ENTITY_ID_FORMAT
+from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.entity import Entity, async_generate_entity_id
+
+UNIT_BAR = 'bar'
+UNIT_HOUR = 'h'
+UNIT_KW = 'kW'
+UNIT_L_MIN = 'L/min'
+UNIT_PERCENT = '%'
+
+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 sensors."""
+ if discovery_info is None:
+ return
+ gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS]
+ sensor_info = {
+ # [device_class, unit, friendly_name]
+ gw_vars.DATA_CONTROL_SETPOINT: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Control Setpoint"],
+ gw_vars.DATA_MASTER_MEMBERID: [None, None, "Thermostat Member ID"],
+ gw_vars.DATA_SLAVE_MEMBERID: [None, None, "Boiler Member ID"],
+ gw_vars.DATA_SLAVE_OEM_FAULT: [None, None, "Boiler OEM Fault Code"],
+ gw_vars.DATA_COOLING_CONTROL: [
+ None, UNIT_PERCENT, "Cooling Control Signal"],
+ gw_vars.DATA_CONTROL_SETPOINT_2: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Control Setpoint 2"],
+ gw_vars.DATA_ROOM_SETPOINT_OVRD: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint Override"],
+ gw_vars.DATA_SLAVE_MAX_RELATIVE_MOD: [
+ None, UNIT_PERCENT, "Boiler Maximum Relative Modulation"],
+ gw_vars.DATA_SLAVE_MAX_CAPACITY: [
+ None, UNIT_KW, "Boiler Maximum Capacity"],
+ gw_vars.DATA_SLAVE_MIN_MOD_LEVEL: [
+ None, UNIT_PERCENT, "Boiler Minimum Modulation Level"],
+ gw_vars.DATA_ROOM_SETPOINT: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint"],
+ gw_vars.DATA_REL_MOD_LEVEL: [
+ None, UNIT_PERCENT, "Relative Modulation Level"],
+ gw_vars.DATA_CH_WATER_PRESS: [
+ None, UNIT_BAR, "Central Heating Water Pressure"],
+ gw_vars.DATA_DHW_FLOW_RATE: [None, UNIT_L_MIN, "Hot Water Flow Rate"],
+ gw_vars.DATA_ROOM_SETPOINT_2: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint 2"],
+ gw_vars.DATA_ROOM_TEMP: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Temperature"],
+ gw_vars.DATA_CH_WATER_TEMP: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS,
+ "Central Heating Water Temperature"],
+ gw_vars.DATA_DHW_TEMP: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water Temperature"],
+ gw_vars.DATA_OUTSIDE_TEMP: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Outside Temperature"],
+ gw_vars.DATA_RETURN_WATER_TEMP: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS,
+ "Return Water Temperature"],
+ gw_vars.DATA_SOLAR_STORAGE_TEMP: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS,
+ "Solar Storage Temperature"],
+ gw_vars.DATA_SOLAR_COLL_TEMP: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS,
+ "Solar Collector Temperature"],
+ gw_vars.DATA_CH_WATER_TEMP_2: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS,
+ "Central Heating 2 Water Temperature"],
+ gw_vars.DATA_DHW_TEMP_2: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water 2 Temperature"],
+ gw_vars.DATA_EXHAUST_TEMP: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Exhaust Temperature"],
+ gw_vars.DATA_SLAVE_DHW_MAX_SETP: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS,
+ "Hot Water Maximum Setpoint"],
+ gw_vars.DATA_SLAVE_DHW_MIN_SETP: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS,
+ "Hot Water Minimum Setpoint"],
+ gw_vars.DATA_SLAVE_CH_MAX_SETP: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS,
+ "Boiler Maximum Central Heating Setpoint"],
+ gw_vars.DATA_SLAVE_CH_MIN_SETP: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS,
+ "Boiler Minimum Central Heating Setpoint"],
+ gw_vars.DATA_DHW_SETPOINT: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water Setpoint"],
+ gw_vars.DATA_MAX_CH_SETPOINT: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS,
+ "Maximum Central Heating Setpoint"],
+ gw_vars.DATA_OEM_DIAG: [None, None, "OEM Diagnostic Code"],
+ gw_vars.DATA_TOTAL_BURNER_STARTS: [
+ None, None, "Total Burner Starts"],
+ gw_vars.DATA_CH_PUMP_STARTS: [
+ None, None, "Central Heating Pump Starts"],
+ gw_vars.DATA_DHW_PUMP_STARTS: [None, None, "Hot Water Pump Starts"],
+ gw_vars.DATA_DHW_BURNER_STARTS: [
+ None, None, "Hot Water Burner Starts"],
+ gw_vars.DATA_TOTAL_BURNER_HOURS: [
+ None, UNIT_HOUR, "Total Burner Hours"],
+ gw_vars.DATA_CH_PUMP_HOURS: [
+ None, UNIT_HOUR, "Central Heating Pump Hours"],
+ gw_vars.DATA_DHW_PUMP_HOURS: [None, UNIT_HOUR, "Hot Water Pump Hours"],
+ gw_vars.DATA_DHW_BURNER_HOURS: [
+ None, UNIT_HOUR, "Hot Water Burner Hours"],
+ gw_vars.DATA_MASTER_OT_VERSION: [
+ None, None, "Thermostat OpenTherm Version"],
+ gw_vars.DATA_SLAVE_OT_VERSION: [
+ None, None, "Boiler OpenTherm Version"],
+ gw_vars.DATA_MASTER_PRODUCT_TYPE: [
+ None, None, "Thermostat Product Type"],
+ gw_vars.DATA_MASTER_PRODUCT_VERSION: [
+ None, None, "Thermostat Product Version"],
+ gw_vars.DATA_SLAVE_PRODUCT_TYPE: [None, None, "Boiler Product Type"],
+ gw_vars.DATA_SLAVE_PRODUCT_VERSION: [
+ None, None, "Boiler Product Version"],
+ gw_vars.OTGW_MODE: [None, None, "Gateway/Monitor Mode"],
+ gw_vars.OTGW_DHW_OVRD: [None, None, "Gateway Hot Water Override Mode"],
+ gw_vars.OTGW_ABOUT: [None, None, "Gateway Firmware Version"],
+ gw_vars.OTGW_BUILD: [None, None, "Gateway Firmware Build"],
+ gw_vars.OTGW_CLOCKMHZ: [None, None, "Gateway Clock Speed"],
+ gw_vars.OTGW_LED_A: [None, None, "Gateway LED A Mode"],
+ gw_vars.OTGW_LED_B: [None, None, "Gateway LED B Mode"],
+ gw_vars.OTGW_LED_C: [None, None, "Gateway LED C Mode"],
+ gw_vars.OTGW_LED_D: [None, None, "Gateway LED D Mode"],
+ gw_vars.OTGW_LED_E: [None, None, "Gateway LED E Mode"],
+ gw_vars.OTGW_LED_F: [None, None, "Gateway LED F Mode"],
+ gw_vars.OTGW_GPIO_A: [None, None, "Gateway GPIO A Mode"],
+ gw_vars.OTGW_GPIO_B: [None, None, "Gateway GPIO B Mode"],
+ gw_vars.OTGW_SB_TEMP: [
+ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS,
+ "Gateway Setback Temperature"],
+ gw_vars.OTGW_SETP_OVRD_MODE: [
+ None, None, "Gateway Room Setpoint Override Mode"],
+ gw_vars.OTGW_SMART_PWR: [None, None, "Gateway Smart Power Mode"],
+ gw_vars.OTGW_THRM_DETECT: [None, None, "Gateway Thermostat Detection"],
+ gw_vars.OTGW_VREF: [None, None, "Gateway Reference Voltage Setting"],
+ }
+ sensors = []
+ for var in discovery_info:
+ device_class = sensor_info[var][0]
+ unit = sensor_info[var][1]
+ friendly_name = sensor_info[var][2]
+ entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, var, hass=hass)
+ sensors.append(
+ OpenThermSensor(entity_id, var, device_class, unit, friendly_name))
+ async_add_entities(sensors)
+
+
+class OpenThermSensor(Entity):
+ """Representation of an OpenTherm Gateway sensor."""
+
+ def __init__(self, entity_id, var, device_class, unit, friendly_name):
+ """Initialize the sensor."""
+ self.entity_id = entity_id
+ self._var = var
+ self._value = None
+ self._device_class = device_class
+ self._unit = unit
+ self._friendly_name = friendly_name
+
+ async def async_added_to_hass(self):
+ """Subscribe to updates from the component."""
+ _LOGGER.debug("Added OpenTherm Gateway 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."""
+ value = status.get(self._var)
+ if isinstance(value, float):
+ value = '{:2.1f}'.format(value)
+ self._value = value
+ self.async_schedule_update_ha_state()
+
+ @property
+ def name(self):
+ """Return the friendly name of the sensor."""
+ return self._friendly_name
+
+ @property
+ def device_class(self):
+ """Return the device class."""
+ return self._device_class
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ return self._value
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self._unit
+
+ @property
+ def should_poll(self):
+ """Return False because entity pushes its state."""
+ return False
diff --git a/homeassistant/components/sensor/openuv.py b/homeassistant/components/sensor/openuv.py
index 22712aa306b..63527db42a6 100644
--- a/homeassistant/components/sensor/openuv.py
+++ b/homeassistant/components/sensor/openuv.py
@@ -64,7 +64,7 @@ class OpenUvSensor(OpenUvEntity):
"""Initialize the sensor."""
super().__init__(openuv)
- self._dispatch_remove = None
+ self._async_unsub_dispatcher_connect = None
self._entry_id = entry_id
self._icon = icon
self._latitude = openuv.client.latitude
@@ -100,16 +100,20 @@ class OpenUvSensor(OpenUvEntity):
"""Return the unit the value is expressed in."""
return self._unit
- @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."""
diff --git a/homeassistant/components/sensor/rflink.py b/homeassistant/components/sensor/rflink.py
index e01c441be84..f3ec776fda8 100644
--- a/homeassistant/components/sensor/rflink.py
+++ b/homeassistant/components/sensor/rflink.py
@@ -2,18 +2,23 @@
Support for Rflink sensors.
For more details about this platform, please refer to the documentation at
-https://home-assistant.io/components/light.rflink/
+https://home-assistant.io/components/sensor.rflink/
"""
-from functools import partial
import logging
+import voluptuous as vol
+
from homeassistant.components.rflink import (
CONF_ALIASES, CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICES,
- DATA_DEVICE_REGISTER, DATA_ENTITY_LOOKUP, DOMAIN, EVENT_KEY_ID,
- EVENT_KEY_SENSOR, EVENT_KEY_UNIT, RflinkDevice, cv, remove_deprecated, vol)
+ DATA_DEVICE_REGISTER, DATA_ENTITY_LOOKUP, EVENT_KEY_ID,
+ EVENT_KEY_SENSOR, EVENT_KEY_UNIT, RflinkDevice, remove_deprecated,
+ SIGNAL_AVAILABILITY, SIGNAL_HANDLE_EVENT, TMP_ENTITY)
+from homeassistant.components.sensor import (
+ PLATFORM_SCHEMA)
+import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
- ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_PLATFORM,
- CONF_UNIT_OF_MEASUREMENT)
+ ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_UNIT_OF_MEASUREMENT)
+from homeassistant.helpers.dispatcher import (async_dispatcher_connect)
DEPENDENCIES = ['rflink']
@@ -27,11 +32,10 @@ SENSOR_ICONS = {
CONF_SENSOR_TYPE = 'sensor_type'
-PLATFORM_SCHEMA = vol.Schema({
- vol.Required(CONF_PLATFORM): DOMAIN,
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean,
- vol.Optional(CONF_DEVICES, default={}): vol.Schema({
- cv.string: {
+ vol.Optional(CONF_DEVICES, default={}): {
+ cv.string: vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_SENSOR_TYPE): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
@@ -40,9 +44,9 @@ PLATFORM_SCHEMA = vol.Schema({
# deprecated config options
vol.Optional(CONF_ALIASSES):
vol.All(cv.ensure_list, [cv.string]),
- },
- }),
-})
+ })
+ },
+}, extra=vol.ALLOW_EXTRA)
def lookup_unit_for_sensor_type(sensor_type):
@@ -56,7 +60,7 @@ def lookup_unit_for_sensor_type(sensor_type):
return UNITS.get(field_abbrev.get(sensor_type))
-def devices_from_config(domain_config, hass=None):
+def devices_from_config(domain_config):
"""Parse configuration and add Rflink sensor devices."""
devices = []
for device_id, config in domain_config[CONF_DEVICES].items():
@@ -64,36 +68,26 @@ def devices_from_config(domain_config, hass=None):
config[ATTR_UNIT_OF_MEASUREMENT] = lookup_unit_for_sensor_type(
config[CONF_SENSOR_TYPE])
remove_deprecated(config)
- device = RflinkSensor(device_id, hass, **config)
+ device = RflinkSensor(device_id, **config)
devices.append(device)
- # Register entity to listen to incoming rflink events
- hass.data[DATA_ENTITY_LOOKUP][
- EVENT_KEY_SENSOR][device_id].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, hass))
+ async_add_entities(devices_from_config(config))
async def add_new_device(event):
"""Check if device is known, otherwise create device entity."""
device_id = event[EVENT_KEY_ID]
- rflinksensor = partial(RflinkSensor, device_id, hass)
- device = rflinksensor(event[EVENT_KEY_SENSOR], event[EVENT_KEY_UNIT])
+ device = RflinkSensor(device_id, event[EVENT_KEY_SENSOR],
+ event[EVENT_KEY_UNIT], initial_event=event)
# Add device entity
async_add_entities([device])
- # Register entity to listen to incoming rflink events
- hass.data[DATA_ENTITY_LOOKUP][
- EVENT_KEY_SENSOR][device_id].append(device)
-
- # Schedule task to process event after entity is created
- hass.async_add_job(device.handle_event, event)
-
if config[CONF_AUTOMATIC_ADD]:
hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_SENSOR] = add_new_device
@@ -101,17 +95,43 @@ async def async_setup_platform(hass, config, async_add_entities,
class RflinkSensor(RflinkDevice):
"""Representation of a Rflink sensor."""
- def __init__(self, device_id, hass, sensor_type, unit_of_measurement,
- **kwargs):
+ def __init__(self, device_id, sensor_type, unit_of_measurement,
+ initial_event=None, **kwargs):
"""Handle sensor specific args and super init."""
self._sensor_type = sensor_type
self._unit_of_measurement = unit_of_measurement
- super().__init__(device_id, hass, **kwargs)
+ super().__init__(device_id, initial_event=initial_event, **kwargs)
def _handle_event(self, event):
"""Domain specific event handler."""
self._state = event['value']
+ async def async_added_to_hass(self):
+ """Register update callback."""
+ # Remove temporary bogus entity_id if added
+ tmp_entity = TMP_ENTITY.format(self._device_id)
+ if tmp_entity in self.hass.data[DATA_ENTITY_LOOKUP][
+ EVENT_KEY_SENSOR][self._device_id]:
+ self.hass.data[DATA_ENTITY_LOOKUP][
+ EVENT_KEY_SENSOR][self._device_id].remove(tmp_entity)
+
+ # Register id and aliases
+ self.hass.data[DATA_ENTITY_LOOKUP][
+ EVENT_KEY_SENSOR][self._device_id].append(self.entity_id)
+ if self._aliases:
+ for _id in self._aliases:
+ self.hass.data[DATA_ENTITY_LOOKUP][
+ EVENT_KEY_SENSOR][_id].append(self.entity_id)
+ async_dispatcher_connect(self.hass, SIGNAL_AVAILABILITY,
+ self._availability_callback)
+ async_dispatcher_connect(self.hass,
+ SIGNAL_HANDLE_EVENT.format(self.entity_id),
+ self.handle_event_callback)
+
+ # Process the initial event now that the entity is created
+ if self._initial_event:
+ self.handle_event_callback(self._initial_event)
+
@property
def unit_of_measurement(self):
"""Return measurement unit."""
diff --git a/homeassistant/components/sensor/ring.py b/homeassistant/components/sensor/ring.py
index 408971c60e1..92c033241e0 100644
--- a/homeassistant/components/sensor/ring.py
+++ b/homeassistant/components/sensor/ring.py
@@ -100,6 +100,7 @@ class RingSensor(Entity):
self._data.name, SENSOR_TYPES.get(self._sensor_type)[0])
self._state = STATE_UNKNOWN
self._tz = str(hass.config.time_zone)
+ self._unique_id = '{}-{}'.format(self._data.id, self._sensor_type)
@property
def name(self):
@@ -111,6 +112,11 @@ class RingSensor(Entity):
"""Return the state of the sensor."""
return self._state
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._unique_id
+
@property
def device_state_attributes(self):
"""Return the state attributes."""
diff --git a/homeassistant/components/sensor/rmvtransport.py b/homeassistant/components/sensor/rmvtransport.py
index 0916765e12d..f9bd65c1a74 100644
--- a/homeassistant/components/sensor/rmvtransport.py
+++ b/homeassistant/components/sensor/rmvtransport.py
@@ -5,15 +5,18 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.rmvtransport/
"""
import logging
+from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION)
+from homeassistant.util import Throttle
-REQUIREMENTS = ['PyRMVtransport==0.1']
+REQUIREMENTS = ['PyRMVtransport==0.1.3']
_LOGGER = logging.getLogger(__name__)
@@ -21,11 +24,12 @@ CONF_NEXT_DEPARTURE = 'next_departure'
CONF_STATION = 'station'
CONF_DESTINATIONS = 'destinations'
-CONF_DIRECTIONS = 'directions'
+CONF_DIRECTION = 'direction'
CONF_LINES = 'lines'
CONF_PRODUCTS = 'products'
CONF_TIME_OFFSET = 'time_offset'
CONF_MAX_JOURNEYS = 'max_journeys'
+CONF_TIMEOUT = 'timeout'
DEFAULT_NAME = 'RMV Journey'
@@ -46,51 +50,61 @@ ICONS = {
}
ATTRIBUTION = "Data provided by opendata.rmv.de"
+SCAN_INTERVAL = timedelta(seconds=60)
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NEXT_DEPARTURE): [{
vol.Required(CONF_STATION): cv.string,
vol.Optional(CONF_DESTINATIONS, default=[]):
vol.All(cv.ensure_list, [cv.string]),
- vol.Optional(CONF_DIRECTIONS, default=[]):
- vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(CONF_DIRECTION): cv.string,
vol.Optional(CONF_LINES, default=[]):
vol.All(cv.ensure_list, [cv.positive_int, cv.string]),
vol.Optional(CONF_PRODUCTS, default=VALID_PRODUCTS):
vol.All(cv.ensure_list, [vol.In(VALID_PRODUCTS)]),
vol.Optional(CONF_TIME_OFFSET, default=0): cv.positive_int,
vol.Optional(CONF_MAX_JOURNEYS, default=5): cv.positive_int,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}]
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}],
+ vol.Optional(CONF_TIMEOUT, default=10): cv.positive_int
})
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
"""Set up the RMV departure sensor."""
+ timeout = config.get(CONF_TIMEOUT)
+
+ session = async_get_clientsession(hass)
+
sensors = []
for next_departure in config.get(CONF_NEXT_DEPARTURE):
sensors.append(
RMVDepartureSensor(
+ session,
next_departure[CONF_STATION],
next_departure.get(CONF_DESTINATIONS),
- next_departure.get(CONF_DIRECTIONS),
+ next_departure.get(CONF_DIRECTION),
next_departure.get(CONF_LINES),
next_departure.get(CONF_PRODUCTS),
next_departure.get(CONF_TIME_OFFSET),
next_departure.get(CONF_MAX_JOURNEYS),
- next_departure.get(CONF_NAME)))
- add_entities(sensors, True)
+ next_departure.get(CONF_NAME),
+ timeout))
+ async_add_entities(sensors, True)
class RMVDepartureSensor(Entity):
"""Implementation of an RMV departure sensor."""
- def __init__(self, station, destinations, directions,
- lines, products, time_offset, max_journeys, name):
+ def __init__(self, session, station, destinations, direction, lines,
+ products, time_offset, max_journeys, name, timeout):
"""Initialize the sensor."""
self._station = station
self._name = name
self._state = None
- self.data = RMVDepartureData(station, destinations, directions, lines,
- products, time_offset, max_journeys)
+ self.data = RMVDepartureData(session, station, destinations,
+ direction, lines, products, time_offset,
+ max_journeys, timeout)
self._icon = ICONS[None]
@property
@@ -134,9 +148,10 @@ class RMVDepartureSensor(Entity):
"""Return the unit this state is expressed in."""
return "min"
- def update(self):
+ async def async_update(self):
"""Get the latest data and update the state."""
- self.data.update()
+ await self.data.async_update()
+
if not self.data.departures:
self._state = None
self._icon = ICONS[None]
@@ -151,27 +166,30 @@ class RMVDepartureSensor(Entity):
class RMVDepartureData:
"""Pull data from the opendata.rmv.de web page."""
- def __init__(self, station_id, destinations, directions,
- lines, products, time_offset, max_journeys):
+ def __init__(self, session, station_id, destinations, direction, lines,
+ products, time_offset, max_journeys, timeout):
"""Initialize the sensor."""
- import RMVtransport
+ from RMVtransport import RMVtransport
+
self.station = None
self._station_id = station_id
self._destinations = destinations
- self._directions = directions
+ self._direction = direction
self._lines = lines
self._products = products
self._time_offset = time_offset
self._max_journeys = max_journeys
- self.rmv = RMVtransport.RMVtransport()
+ self.rmv = RMVtransport(session, timeout)
self.departures = []
- def update(self):
+ @Throttle(SCAN_INTERVAL)
+ async def async_update(self):
"""Update the connection data."""
try:
- _data = self.rmv.get_departures(self._station_id,
- products=self._products,
- maxJourneys=50)
+ _data = await self.rmv.get_departures(self._station_id,
+ products=self._products,
+ directionId=self._direction,
+ maxJourneys=50)
except ValueError:
self.departures = []
_LOGGER.warning("Returned data not understood")
diff --git a/homeassistant/components/sensor/rtorrent.py b/homeassistant/components/sensor/rtorrent.py
new file mode 100644
index 00000000000..f71b9c6dbdb
--- /dev/null
+++ b/homeassistant/components/sensor/rtorrent.py
@@ -0,0 +1,127 @@
+"""Support for monitoring the rtorrent BitTorrent client API."""
+import logging
+import xmlrpc.client
+
+import voluptuous as vol
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import (
+ CONF_URL, CONF_NAME,
+ CONF_MONITORED_VARIABLES, STATE_IDLE)
+from homeassistant.helpers.entity import Entity
+import homeassistant.helpers.config_validation as cv
+from homeassistant.exceptions import PlatformNotReady
+
+_LOGGER = logging.getLogger(__name__)
+
+SENSOR_TYPE_CURRENT_STATUS = 'current_status'
+SENSOR_TYPE_DOWNLOAD_SPEED = 'download_speed'
+SENSOR_TYPE_UPLOAD_SPEED = 'upload_speed'
+
+DEFAULT_NAME = 'rtorrent'
+SENSOR_TYPES = {
+ SENSOR_TYPE_CURRENT_STATUS: ['Status', None],
+ SENSOR_TYPE_DOWNLOAD_SPEED: ['Down Speed', 'kB/s'],
+ SENSOR_TYPE_UPLOAD_SPEED: ['Up Speed', 'kB/s'],
+}
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_URL): cv.url,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_MONITORED_VARIABLES, default=[]): vol.All(
+ cv.ensure_list, [vol.In(SENSOR_TYPES)]),
+})
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the rtorrent sensors."""
+ url = config[CONF_URL]
+ name = config[CONF_NAME]
+
+ try:
+ rtorrent = xmlrpc.client.ServerProxy(url)
+ except (xmlrpc.client.ProtocolError, ConnectionRefusedError):
+ _LOGGER.error("Connection to rtorrent daemon failed")
+ raise PlatformNotReady
+ dev = []
+ for variable in config[CONF_MONITORED_VARIABLES]:
+ dev.append(RTorrentSensor(variable, rtorrent, name))
+
+ add_entities(dev)
+
+
+def format_speed(speed):
+ """Return a bytes/s measurement as a human readable string."""
+ kb_spd = float(speed) / 1024
+ return round(kb_spd, 2 if kb_spd < 0.1 else 1)
+
+
+class RTorrentSensor(Entity):
+ """Representation of an rtorrent sensor."""
+
+ def __init__(self, sensor_type, rtorrent_client, client_name):
+ """Initialize the sensor."""
+ self._name = SENSOR_TYPES[sensor_type][0]
+ self.client = rtorrent_client
+ self.type = sensor_type
+ self.client_name = client_name
+ self._state = None
+ self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
+ self.data = None
+ self._available = False
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return '{} {}'.format(self.client_name, self._name)
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def available(self):
+ """Return true if device is available."""
+ return self._available
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this entity, if any."""
+ return self._unit_of_measurement
+
+ def update(self):
+ """Get the latest data from rtorrent and updates the state."""
+ multicall = xmlrpc.client.MultiCall(self.client)
+ multicall.throttle.global_up.rate()
+ multicall.throttle.global_down.rate()
+
+ try:
+ self.data = multicall()
+ self._available = True
+ except (xmlrpc.client.ProtocolError, ConnectionRefusedError):
+ _LOGGER.error("Connection to rtorrent lost")
+ self._available = False
+ return
+
+ upload = self.data[0]
+ download = self.data[1]
+
+ if self.type == SENSOR_TYPE_CURRENT_STATUS:
+ if self.data:
+ if upload > 0 and download > 0:
+ self._state = 'Up/Down'
+ elif upload > 0 and download == 0:
+ self._state = 'Seeding'
+ elif upload == 0 and download > 0:
+ self._state = 'Downloading'
+ else:
+ self._state = STATE_IDLE
+ else:
+ self._state = None
+
+ if self.data:
+ if self.type == SENSOR_TYPE_DOWNLOAD_SPEED:
+ self._state = format_speed(download)
+ elif self.type == SENSOR_TYPE_UPLOAD_SPEED:
+ self._state = format_speed(upload)
diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py
index bd74bcaeb2c..1cce17cf64a 100644
--- a/homeassistant/components/sensor/shodan.py
+++ b/homeassistant/components/sensor/shodan.py
@@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['shodan==1.10.2']
+REQUIREMENTS = ['shodan==1.10.4']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py
index ee6cad61e20..a08eec56e17 100644
--- a/homeassistant/components/sensor/speedtest.py
+++ b/homeassistant/components/sensor/speedtest.py
@@ -32,7 +32,6 @@ CONF_ATTRIBUTION = "Data retrieved from Speedtest by Ookla"
CONF_SECOND = 'second'
CONF_MINUTE = 'minute'
CONF_HOUR = 'hour'
-CONF_DAY = 'day'
CONF_SERVER_ID = 'server_id'
CONF_MANUAL = 'manual'
@@ -47,8 +46,6 @@ SENSOR_TYPES = {
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]),
- vol.Optional(CONF_DAY):
- vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(1, 31))]),
vol.Optional(CONF_HOUR):
vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]),
vol.Optional(CONF_MANUAL, default=False): cv.boolean,
@@ -156,8 +153,7 @@ class SpeedtestData:
if not config.get(CONF_MANUAL):
track_time_change(
hass, self.update, second=config.get(CONF_SECOND),
- minute=config.get(CONF_MINUTE), hour=config.get(CONF_HOUR),
- day=config.get(CONF_DAY))
+ minute=config.get(CONF_MINUTE), hour=config.get(CONF_HOUR))
def update(self, now):
"""Get the latest data from speedtest.net."""
diff --git a/homeassistant/components/sensor/syncthru.py b/homeassistant/components/sensor/syncthru.py
index 45a529012e3..862efb63fd7 100644
--- a/homeassistant/components/sensor/syncthru.py
+++ b/homeassistant/components/sensor/syncthru.py
@@ -115,6 +115,13 @@ class SyncThruSensor(Entity):
self._name = name
self._icon = 'mdi:printer'
self._unit_of_measurement = None
+ self._id_suffix = ''
+
+ @property
+ def unique_id(self):
+ """Return unique ID for the sensor."""
+ serial = self.syncthru.serial_number()
+ return serial + self._id_suffix if serial else super().unique_id
@property
def name(self):
@@ -145,6 +152,11 @@ class SyncThruSensor(Entity):
class SyncThruMainSensor(SyncThruSensor):
"""Implementation of the main sensor, monitoring the general state."""
+ def __init__(self, syncthru, name):
+ """Initialize the sensor."""
+ super().__init__(syncthru, name)
+ self._id_suffix = '_main'
+
def update(self):
"""Get the latest data from SyncThru and update the state."""
self.syncthru.update()
@@ -160,6 +172,7 @@ class SyncThruTonerSensor(SyncThruSensor):
self._name = "{} Toner {}".format(name, color)
self._color = color
self._unit_of_measurement = '%'
+ self._id_suffix = '_toner_{}'.format(color)
def update(self):
"""Get the latest data from SyncThru and update the state."""
@@ -180,6 +193,7 @@ class SyncThruDrumSensor(SyncThruSensor):
self._name = "{} Drum {}".format(name, color)
self._color = color
self._unit_of_measurement = '%'
+ self._id_suffix = '_drum_{}'.format(color)
def update(self):
"""Get the latest data from SyncThru and update the state."""
@@ -199,6 +213,7 @@ class SyncThruInputTraySensor(SyncThruSensor):
super().__init__(syncthru, name)
self._name = "{} Tray {}".format(name, number)
self._number = number
+ self._id_suffix = '_tray_{}'.format(number)
def update(self):
"""Get the latest data from SyncThru and update the state."""
@@ -220,6 +235,7 @@ class SyncThruOutputTraySensor(SyncThruSensor):
super().__init__(syncthru, name)
self._name = "{} Output Tray {}".format(name, number)
self._number = number
+ self._id_suffix = '_output_tray_{}'.format(number)
def update(self):
"""Get the latest data from SyncThru and update the state."""
diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py
index 77b3759d5fc..3fa45935617 100644
--- a/homeassistant/components/sensor/template.py
+++ b/homeassistant/components/sensor/template.py
@@ -57,22 +57,36 @@ async def async_setup_platform(hass, config, async_add_entities,
entity_ids = set()
manual_entity_ids = device_config.get(ATTR_ENTITY_ID)
+ invalid_templates = []
- for template in (state_template, icon_template,
- entity_picture_template, friendly_name_template):
+ for tpl_name, template in (
+ (CONF_VALUE_TEMPLATE, state_template),
+ (CONF_ICON_TEMPLATE, icon_template),
+ (CONF_ENTITY_PICTURE_TEMPLATE, entity_picture_template),
+ (CONF_FRIENDLY_NAME_TEMPLATE, friendly_name_template),
+ ):
if template is None:
continue
template.hass = hass
- if entity_ids == MATCH_ALL or manual_entity_ids is not None:
+ if manual_entity_ids is not None:
continue
template_entity_ids = template.extract_entities()
if template_entity_ids == MATCH_ALL:
entity_ids = MATCH_ALL
- else:
+ # Cut off _template from name
+ invalid_templates.append(tpl_name[:-9])
+ elif entity_ids != MATCH_ALL:
entity_ids |= set(template_entity_ids)
+ if invalid_templates:
+ _LOGGER.warning(
+ 'Template sensor %s has no entity ids configured to track nor'
+ ' were we able to extract the entities to track from the %s '
+ 'template(s). This entity will only be able to be updated '
+ 'manually.', device, ', '.join(invalid_templates))
+
if manual_entity_ids is not None:
entity_ids = manual_entity_ids
elif entity_ids != MATCH_ALL:
@@ -131,8 +145,10 @@ class SensorTemplate(Entity):
@callback
def template_sensor_startup(event):
"""Update template on startup."""
- async_track_state_change(
- self.hass, self._entities, template_sensor_state_listener)
+ if self._entities != MATCH_ALL:
+ # Track state change only for valid templates
+ async_track_state_change(
+ self.hass, self._entities, template_sensor_state_listener)
self.async_schedule_update_ha_state(True)
diff --git a/homeassistant/components/sensor/thermoworks_smoke.py b/homeassistant/components/sensor/thermoworks_smoke.py
new file mode 100644
index 00000000000..e81a3974176
--- /dev/null
+++ b/homeassistant/components/sensor/thermoworks_smoke.py
@@ -0,0 +1,180 @@
+"""
+Support for getting the state of a Thermoworks Smoke Thermometer.
+
+Requires Smoke Gateway Wifi with an internet connection.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.thermoworks_smoke/
+"""
+import logging
+
+from requests import RequestException
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import TEMP_FAHRENHEIT, CONF_EMAIL, CONF_PASSWORD,\
+ CONF_MONITORED_CONDITIONS, CONF_EXCLUDE, ATTR_BATTERY_LEVEL
+from homeassistant.helpers.entity import Entity
+
+REQUIREMENTS = ['thermoworks_smoke==0.1.7', 'stringcase==1.2.0']
+
+_LOGGER = logging.getLogger(__name__)
+
+PROBE_1 = 'probe1'
+PROBE_2 = 'probe2'
+PROBE_1_MIN = 'probe1_min'
+PROBE_1_MAX = 'probe1_max'
+PROBE_2_MIN = 'probe2_min'
+PROBE_2_MAX = 'probe2_max'
+BATTERY_LEVEL = 'battery'
+FIRMWARE = 'firmware'
+
+SERIAL_REGEX = '^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$'
+
+# map types to labels
+SENSOR_TYPES = {
+ PROBE_1: 'Probe 1',
+ PROBE_2: 'Probe 2',
+ PROBE_1_MIN: 'Probe 1 Min',
+ PROBE_1_MAX: 'Probe 1 Max',
+ PROBE_2_MIN: 'Probe 2 Min',
+ PROBE_2_MAX: 'Probe 2 Max',
+}
+
+# exclude these keys from thermoworks data
+EXCLUDE_KEYS = [
+ FIRMWARE
+]
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_EMAIL): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_MONITORED_CONDITIONS, default=[PROBE_1, PROBE_2]):
+ vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
+ vol.Optional(CONF_EXCLUDE, default=[]):
+ vol.All(cv.ensure_list, [cv.matches_regex(SERIAL_REGEX)]),
+})
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the thermoworks sensor."""
+ import thermoworks_smoke
+ from requests.exceptions import HTTPError
+
+ email = config[CONF_EMAIL]
+ password = config[CONF_PASSWORD]
+ monitored_variables = config[CONF_MONITORED_CONDITIONS]
+ excluded = config[CONF_EXCLUDE]
+
+ try:
+ mgr = thermoworks_smoke.initialize_app(email, password, True, excluded)
+
+ # list of sensor devices
+ dev = []
+
+ # get list of registered devices
+ for serial in mgr.serials():
+ for variable in monitored_variables:
+ dev.append(ThermoworksSmokeSensor(variable, serial, mgr))
+
+ add_entities(dev, True)
+ except HTTPError as error:
+ msg = "{}".format(error.strerror)
+ if 'EMAIL_NOT_FOUND' in msg or \
+ 'INVALID_PASSWORD' in msg:
+ _LOGGER.error("Invalid email and password combination")
+ else:
+ _LOGGER.error(msg)
+
+
+class ThermoworksSmokeSensor(Entity):
+ """Implementation of a thermoworks smoke sensor."""
+
+ def __init__(self, sensor_type, serial, mgr):
+ """Initialize the sensor."""
+ self._name = "{name} {sensor}".format(
+ name=mgr.name(serial), sensor=SENSOR_TYPES[sensor_type])
+ self.type = sensor_type
+ self._state = None
+ self._attributes = {}
+ self._unit_of_measurement = TEMP_FAHRENHEIT
+ self._unique_id = "{serial}-{type}".format(
+ serial=serial, type=sensor_type)
+ self.serial = serial
+ self.mgr = mgr
+ self.update_unit()
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def unique_id(self):
+ """Return the unique id for the sensor."""
+ return self._unique_id
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ return self._attributes
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this sensor."""
+ return self._unit_of_measurement
+
+ def update_unit(self):
+ """Set the units from the data."""
+ if PROBE_2 in self.type:
+ self._unit_of_measurement = self.mgr.units(self.serial, PROBE_2)
+ else:
+ self._unit_of_measurement = self.mgr.units(self.serial, PROBE_1)
+
+ def update(self):
+ """Get the monitored data from firebase."""
+ from stringcase import camelcase, snakecase
+ try:
+ values = self.mgr.data(self.serial)
+
+ # set state from data based on type of sensor
+ self._state = values.get(camelcase(self.type))
+
+ # set units
+ self.update_unit()
+
+ # set basic attributes for all sensors
+ self._attributes = {
+ 'time': values['time'],
+ 'localtime': values['localtime']
+ }
+
+ # set extended attributes for main probe sensors
+ if self.type in [PROBE_1, PROBE_2]:
+ for key, val in values.items():
+ # add all attributes that don't contain any probe name
+ # or contain a matching probe name
+ if (
+ (self.type == PROBE_1 and key.find(PROBE_2) == -1)
+ or
+ (self.type == PROBE_2 and key.find(PROBE_1) == -1)
+ ):
+ if key == BATTERY_LEVEL:
+ key = ATTR_BATTERY_LEVEL
+ else:
+ # strip probe label and convert to snake_case
+ key = snakecase(key.replace(self.type, ''))
+ # add to attrs
+ if key and key not in EXCLUDE_KEYS:
+ self._attributes[key] = val
+ # store actual unit because attributes are not converted
+ self._attributes['unit_of_min_max'] = self._unit_of_measurement
+
+ except (RequestException, ValueError, KeyError):
+ _LOGGER.warning("Could not update status for %s", self.name)
diff --git a/homeassistant/components/sensor/transport_nsw.py b/homeassistant/components/sensor/transport_nsw.py
new file mode 100644
index 00000000000..08a2907748c
--- /dev/null
+++ b/homeassistant/components/sensor/transport_nsw.py
@@ -0,0 +1,125 @@
+"""
+Transport NSW (AU) sensor to query next leave event for a specified stop.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.transport_nsw/
+"""
+import logging
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import (CONF_NAME, CONF_API_KEY, ATTR_ATTRIBUTION)
+
+REQUIREMENTS = ['PyTransportNSW==0.0.8']
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_STOP_ID = 'stop_id'
+ATTR_ROUTE = 'route'
+ATTR_DUE_IN = 'due'
+ATTR_DELAY = 'delay'
+ATTR_REAL_TIME = 'real_time'
+
+CONF_ATTRIBUTION = "Data provided by Transport NSW"
+CONF_STOP_ID = 'stop_id'
+CONF_ROUTE = 'route'
+
+DEFAULT_NAME = "Next Bus"
+ICON = "mdi:bus"
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_STOP_ID): cv.string,
+ vol.Required(CONF_API_KEY): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_ROUTE, default=""): cv.string,
+})
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the Transport NSW sensor."""
+ stop_id = config[CONF_STOP_ID]
+ api_key = config[CONF_API_KEY]
+ route = config.get(CONF_ROUTE)
+ name = config.get(CONF_NAME)
+
+ data = PublicTransportData(stop_id, route, api_key)
+ add_entities([TransportNSWSensor(data, stop_id, name)], True)
+
+
+class TransportNSWSensor(Entity):
+ """Implementation of an Transport NSW sensor."""
+
+ def __init__(self, data, stop_id, name):
+ """Initialize the sensor."""
+ self.data = data
+ self._name = name
+ self._stop_id = stop_id
+ self._times = self._state = None
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ if self._times is not None:
+ return {
+ ATTR_DUE_IN: self._times[ATTR_DUE_IN],
+ ATTR_STOP_ID: self._stop_id,
+ ATTR_ROUTE: self._times[ATTR_ROUTE],
+ ATTR_DELAY: self._times[ATTR_DELAY],
+ ATTR_REAL_TIME: self._times[ATTR_REAL_TIME],
+ ATTR_ATTRIBUTION: CONF_ATTRIBUTION
+ }
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit this state is expressed in."""
+ return 'min'
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend, if any."""
+ return ICON
+
+ def update(self):
+ """Get the latest data from Transport NSW and update the states."""
+ self.data.update()
+ self._times = self.data.info
+ self._state = self._times[ATTR_DUE_IN]
+
+
+class PublicTransportData:
+ """The Class for handling the data retrieval."""
+
+ def __init__(self, stop_id, route, api_key):
+ """Initialize the data object."""
+ import TransportNSW
+ self._stop_id = stop_id
+ self._route = route
+ self._api_key = api_key
+ self.info = {ATTR_ROUTE: self._route,
+ ATTR_DUE_IN: 'n/a',
+ ATTR_DELAY: 'n/a',
+ ATTR_REAL_TIME: 'n/a'}
+ self.tnsw = TransportNSW.TransportNSW()
+
+ def update(self):
+ """Get the next leave time."""
+ _data = self.tnsw.get_departures(self._stop_id,
+ self._route,
+ self._api_key)
+ self.info = {ATTR_ROUTE: _data['route'],
+ ATTR_DUE_IN: _data['due'],
+ ATTR_DELAY: _data['delay'],
+ ATTR_REAL_TIME: _data['real_time']}
diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/sensor/xiaomi_aqara.py
index 31366fe0097..33517e957b9 100644
--- a/homeassistant/components/sensor/xiaomi_aqara.py
+++ b/homeassistant/components/sensor/xiaomi_aqara.py
@@ -41,6 +41,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
elif device['model'] in ['gateway', 'gateway.v3', 'acpartner.v3']:
devices.append(XiaomiSensor(device, 'Illumination',
'illumination', gateway))
+ elif device['model'] in ['vibration']:
+ devices.append(XiaomiSensor(device, 'Bed Activity',
+ 'bed_activity', gateway))
+ devices.append(XiaomiSensor(device, 'Tilt Angle',
+ 'final_tilt_angle', gateway))
+ devices.append(XiaomiSensor(device, 'Coordination',
+ 'coordination', gateway))
+ else:
+ _LOGGER.warning("Unmapped Device Model ")
add_entities(devices)
@@ -84,6 +93,9 @@ class XiaomiSensor(XiaomiDevice):
value = data.get(self._data_key)
if value is None:
return False
+ if self._data_key in ['coordination', 'status']:
+ self._state = value
+ return True
value = float(value)
if self._data_key in ['temperature', 'humidity', 'pressure']:
value /= 100
diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py
index 15af57bf46b..dd0785120ea 100644
--- a/homeassistant/components/sensor/xiaomi_miio.py
+++ b/homeassistant/components/sensor/xiaomi_miio.py
@@ -8,11 +8,13 @@ import logging
import voluptuous as vol
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
+from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN)
-from homeassistant.exceptions import PlatformNotReady
+
+REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45']
_LOGGER = logging.getLogger(__name__)
@@ -25,8 +27,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
-REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41']
-
ATTR_POWER = 'power'
ATTR_CHARGING = 'charging'
ATTR_BATTERY_LEVEL = 'battery_level'
diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py
index 0d5b40d1d98..9a9de0d6cf2 100644
--- a/homeassistant/components/sensor/zha.py
+++ b/homeassistant/components/sensor/zha.py
@@ -36,7 +36,9 @@ async def make_sensor(discovery_info):
from zigpy.zcl.clusters.smartenergy import Metering
from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
in_clusters = discovery_info['in_clusters']
- if RelativeHumidity.cluster_id in in_clusters:
+ if 'sub_component' in discovery_info:
+ sensor = discovery_info['sub_component'](**discovery_info)
+ elif RelativeHumidity.cluster_id in in_clusters:
sensor = RelativeHumiditySensor(**discovery_info)
elif TemperatureMeasurement.cluster_id in in_clusters:
sensor = TemperatureSensor(**discovery_info)
diff --git a/homeassistant/components/sensor/zoneminder.py b/homeassistant/components/sensor/zoneminder.py
index d4164bbf721..8e4b57f1f38 100644
--- a/homeassistant/components/sensor/zoneminder.py
+++ b/homeassistant/components/sensor/zoneminder.py
@@ -54,6 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for sensor in config[CONF_MONITORED_CONDITIONS]:
sensors.append(ZMSensorEvents(monitor, include_archived, sensor))
+ sensors.append(ZMSensorRunState(zm_client))
add_entities(sensors)
@@ -114,3 +115,26 @@ class ZMSensorEvents(Entity):
"""Update the sensor."""
self._state = self._monitor.get_events(
self.time_period, self._include_archived)
+
+
+class ZMSensorRunState(Entity):
+ """Get the ZoneMinder run state."""
+
+ def __init__(self, client):
+ """Initialize run state sensor."""
+ self._state = None
+ self._client = client
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return 'Run State'
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ def update(self):
+ """Update the sensor."""
+ self._state = self._client.get_active_state()
diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py
index c6356efe157..ce25b61146b 100644
--- a/homeassistant/components/sensor/zwave.py
+++ b/homeassistant/components/sensor/zwave.py
@@ -5,14 +5,31 @@ For more details about this platform, please refer to the documentation
at https://home-assistant.io/components/sensor.zwave/
"""
import logging
+from homeassistant.core import callback
from homeassistant.components.sensor import DOMAIN
from homeassistant.components import zwave
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
-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__)
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Old method of setting up Z-Wave sensors."""
+ pass
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Z-Wave Sensor from Config Entry."""
+ @callback
+ def async_add_sensor(sensor):
+ """Add Z-Wave Sensor."""
+ async_add_entities([sensor])
+
+ async_dispatcher_connect(hass, 'zwave_new_sensor', async_add_sensor)
+
+
def get_device(node, values, **kwargs):
"""Create Z-Wave entity device."""
# Generic Device mappings
diff --git a/homeassistant/components/simplisafe/.translations/en.json b/homeassistant/components/simplisafe/.translations/en.json
new file mode 100644
index 00000000000..b000335af8f
--- /dev/null
+++ b/homeassistant/components/simplisafe/.translations/en.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Account already registered",
+ "invalid_credentials": "Invalid credentials"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "code": "Code (for Home Assistant)",
+ "password": "Password",
+ "username": "Email Address"
+ },
+ "title": "Fill in your information"
+ }
+ },
+ "title": "SimpliSafe"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/simplisafe/.translations/es.json b/homeassistant/components/simplisafe/.translations/es.json
new file mode 100644
index 00000000000..12d0f63356f
--- /dev/null
+++ b/homeassistant/components/simplisafe/.translations/es.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Cuenta ya registrada",
+ "invalid_credentials": "Credenciales no v\u00e1lidas"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "code": "C\u00f3digo (para Home Assistant)",
+ "password": "Contrase\u00f1a",
+ "username": "Direcci\u00f3n de correo electr\u00f3nico"
+ },
+ "title": "Rellene sus datos"
+ }
+ },
+ "title": "SimpliSafe"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/simplisafe/.translations/hu.json b/homeassistant/components/simplisafe/.translations/hu.json
index ff2c2fc87b5..103bf4e18d0 100644
--- a/homeassistant/components/simplisafe/.translations/hu.json
+++ b/homeassistant/components/simplisafe/.translations/hu.json
@@ -1,10 +1,15 @@
{
"config": {
+ "error": {
+ "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok"
+ },
"step": {
"user": {
"data": {
- "password": "Jelsz\u00f3"
- }
+ "password": "Jelsz\u00f3",
+ "username": "Email c\u00edm"
+ },
+ "title": "T\u00f6ltsd ki az adataid"
}
}
}
diff --git a/homeassistant/components/simplisafe/.translations/nl.json b/homeassistant/components/simplisafe/.translations/nl.json
new file mode 100644
index 00000000000..c84593c0b23
--- /dev/null
+++ b/homeassistant/components/simplisafe/.translations/nl.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Account bestaat al",
+ "invalid_credentials": "Ongeldige gebruikersgegevens"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "code": "Code (voor Home Assistant)",
+ "password": "Wachtwoord",
+ "username": "E-mailadres"
+ },
+ "title": "Vul uw gegevens in"
+ }
+ },
+ "title": "SimpliSafe"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/simplisafe/.translations/pt.json b/homeassistant/components/simplisafe/.translations/pt.json
new file mode 100644
index 00000000000..47929161976
--- /dev/null
+++ b/homeassistant/components/simplisafe/.translations/pt.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Conta j\u00e1 registada",
+ "invalid_credentials": "Credenciais inv\u00e1lidas"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "code": "C\u00f3digo (para Home Assistant)",
+ "password": "Palavra-passe",
+ "username": "Endere\u00e7o de e-mail"
+ },
+ "title": "Preencha as suas informa\u00e7\u00f5es"
+ }
+ },
+ "title": "SimpliSafe"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/simplisafe/.translations/ro.json b/homeassistant/components/simplisafe/.translations/ro.json
index 7046b0992b1..b7e281a2bc2 100644
--- a/homeassistant/components/simplisafe/.translations/ro.json
+++ b/homeassistant/components/simplisafe/.translations/ro.json
@@ -7,11 +7,13 @@
"step": {
"user": {
"data": {
+ "code": "Cod (pentru Home Assistant)",
"password": "Parola",
"username": "Adresa de email"
},
"title": "Completa\u021bi informa\u021biile dvs."
}
- }
+ },
+ "title": "SimpliSafe"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/simplisafe/.translations/tr.json b/homeassistant/components/simplisafe/.translations/tr.json
new file mode 100644
index 00000000000..ec84b1b7c1c
--- /dev/null
+++ b/homeassistant/components/simplisafe/.translations/tr.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "E-posta adresi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py
new file mode 100644
index 00000000000..de6277c2ef1
--- /dev/null
+++ b/homeassistant/components/simplisafe/__init__.py
@@ -0,0 +1,142 @@
+"""
+Support for SimpliSafe alarm systems.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/simplisafe/
+"""
+import logging
+from datetime import timedelta
+
+import voluptuous as vol
+
+from homeassistant.config_entries import SOURCE_IMPORT
+from homeassistant.const import (
+ CONF_CODE, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME)
+from homeassistant.core import callback
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.event import async_track_time_interval
+
+from homeassistant.helpers import config_validation as cv
+
+from .config_flow import configured_instances
+from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE
+
+REQUIREMENTS = ['simplisafe-python==3.1.12']
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_ACCOUNTS = 'accounts'
+
+DATA_LISTENER = 'listener'
+
+ACCOUNT_CONFIG_SCHEMA = vol.Schema({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_CODE): cv.string,
+ vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
+ cv.time_period
+})
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Optional(CONF_ACCOUNTS):
+ vol.All(cv.ensure_list, [ACCOUNT_CONFIG_SCHEMA]),
+ }),
+}, extra=vol.ALLOW_EXTRA)
+
+
+@callback
+def _async_save_refresh_token(hass, config_entry, token):
+ hass.config_entries.async_update_entry(
+ config_entry, data={
+ **config_entry.data, CONF_TOKEN: token
+ })
+
+
+async def async_setup(hass, config):
+ """Set up the SimpliSafe component."""
+ hass.data[DOMAIN] = {}
+ hass.data[DOMAIN][DATA_CLIENT] = {}
+ hass.data[DOMAIN][DATA_LISTENER] = {}
+
+ if DOMAIN not in config:
+ return True
+
+ conf = config[DOMAIN]
+
+ for account in conf[CONF_ACCOUNTS]:
+ if account[CONF_USERNAME] in configured_instances(hass):
+ continue
+
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={'source': SOURCE_IMPORT},
+ data={
+ CONF_USERNAME: account[CONF_USERNAME],
+ CONF_PASSWORD: account[CONF_PASSWORD],
+ CONF_CODE: account.get(CONF_CODE),
+ CONF_SCAN_INTERVAL: account[CONF_SCAN_INTERVAL],
+ }))
+
+ return True
+
+
+async def async_setup_entry(hass, config_entry):
+ """Set up SimpliSafe as config entry."""
+ from simplipy import API
+ from simplipy.errors import InvalidCredentialsError, SimplipyError
+
+ websession = aiohttp_client.async_get_clientsession(hass)
+
+ try:
+ simplisafe = await API.login_via_token(
+ config_entry.data[CONF_TOKEN], websession)
+ except InvalidCredentialsError:
+ _LOGGER.error('Invalid credentials provided')
+ return False
+ except SimplipyError as err:
+ _LOGGER.error('Config entry failed: %s', err)
+ raise ConfigEntryNotReady
+
+ _async_save_refresh_token(hass, config_entry, simplisafe.refresh_token)
+
+ systems = await simplisafe.get_systems()
+ hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = systems
+
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(
+ config_entry, 'alarm_control_panel'))
+
+ async def refresh(event_time):
+ """Refresh data from the SimpliSafe account."""
+ for system in systems:
+ _LOGGER.debug('Updating system data: %s', system.system_id)
+ await system.update()
+ async_dispatcher_send(hass, TOPIC_UPDATE.format(system.system_id))
+
+ if system.api.refresh_token_dirty:
+ _async_save_refresh_token(
+ hass, config_entry, system.api.refresh_token)
+
+ hass.data[DOMAIN][DATA_LISTENER][
+ config_entry.entry_id] = async_track_time_interval(
+ hass,
+ refresh,
+ timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL]))
+
+ return True
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a SimpliSafe config entry."""
+ await hass.config_entries.async_forward_entry_unload(
+ entry, 'alarm_control_panel')
+
+ hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id)
+ remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id)
+ remove_listener()
+
+ return True
diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py
new file mode 100644
index 00000000000..0a59dcb3e1d
--- /dev/null
+++ b/homeassistant/components/simplisafe/config_flow.py
@@ -0,0 +1,80 @@
+"""Config flow to configure the SimpliSafe component."""
+
+from collections import OrderedDict
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.core import callback
+from homeassistant.const import (
+ CONF_CODE, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME)
+from homeassistant.helpers import aiohttp_client
+
+from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
+
+
+@callback
+def configured_instances(hass):
+ """Return a set of configured SimpliSafe instances."""
+ return set(
+ entry.data[CONF_USERNAME]
+ for entry in hass.config_entries.async_entries(DOMAIN))
+
+
+@config_entries.HANDLERS.register(DOMAIN)
+class SimpliSafeFlowHandler(config_entries.ConfigFlow):
+ """Handle a SimpliSafe config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ def __init__(self):
+ """Initialize the config flow."""
+ self.data_schema = OrderedDict()
+ self.data_schema[vol.Required(CONF_USERNAME)] = str
+ self.data_schema[vol.Required(CONF_PASSWORD)] = str
+ self.data_schema[vol.Optional(CONF_CODE)] = str
+
+ async def _show_form(self, errors=None):
+ """Show the form to the user."""
+ return self.async_show_form(
+ step_id='user',
+ data_schema=vol.Schema(self.data_schema),
+ errors=errors if errors else {},
+ )
+
+ async def async_step_import(self, import_config):
+ """Import a config entry from configuration.yaml."""
+ return await self.async_step_user(import_config)
+
+ async def async_step_user(self, user_input=None):
+ """Handle the start of the config flow."""
+ from simplipy import API
+ from simplipy.errors import SimplipyError
+
+ if not user_input:
+ return await self._show_form()
+
+ if user_input[CONF_USERNAME] in configured_instances(self.hass):
+ return await self._show_form({CONF_USERNAME: 'identifier_exists'})
+
+ username = user_input[CONF_USERNAME]
+ websession = aiohttp_client.async_get_clientsession(self.hass)
+
+ try:
+ simplisafe = await API.login_via_credentials(
+ username, user_input[CONF_PASSWORD], websession)
+ except SimplipyError:
+ return await self._show_form({'base': 'invalid_credentials'})
+
+ scan_interval = user_input.get(
+ CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
+
+ return self.async_create_entry(
+ title=user_input[CONF_USERNAME],
+ data={
+ CONF_USERNAME: username,
+ CONF_TOKEN: simplisafe.refresh_token,
+ CONF_SCAN_INTERVAL: scan_interval.seconds,
+ },
+ )
diff --git a/homeassistant/components/simplisafe/const.py b/homeassistant/components/simplisafe/const.py
new file mode 100644
index 00000000000..437197878e0
--- /dev/null
+++ b/homeassistant/components/simplisafe/const.py
@@ -0,0 +1,10 @@
+"""Define constants for the SimpliSafe component."""
+from datetime import timedelta
+
+DOMAIN = 'simplisafe'
+
+DATA_CLIENT = 'client'
+
+DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
+
+TOPIC_UPDATE = 'update_{0}'
diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json
new file mode 100644
index 00000000000..5df0cf400d4
--- /dev/null
+++ b/homeassistant/components/simplisafe/strings.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "title": "SimpliSafe",
+ "step": {
+ "user": {
+ "title": "Fill in your information",
+ "data": {
+ "username": "Email Address",
+ "password": "Password",
+ "code": "Code (for Home Assistant)"
+ }
+ }
+ },
+ "error": {
+ "identifier_exists": "Account already registered",
+ "invalid_credentials": "Invalid credentials"
+ }
+ }
+}
diff --git a/homeassistant/components/smhi/.translations/es.json b/homeassistant/components/smhi/.translations/es.json
new file mode 100644
index 00000000000..627c534f6dd
--- /dev/null
+++ b/homeassistant/components/smhi/.translations/es.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "Nombre ya existe",
+ "wrong_location": "Ubicaci\u00f3n Suecia solamente"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "name": "Nombre"
+ },
+ "title": "Ubicaci\u00f3n en Suecia"
+ }
+ },
+ "title": "Servicio meteorol\u00f3gico sueco (SMHI)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smhi/.translations/fr.json b/homeassistant/components/smhi/.translations/fr.json
new file mode 100644
index 00000000000..d1378f183d5
--- /dev/null
+++ b/homeassistant/components/smhi/.translations/fr.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Nom"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smhi/.translations/hu.json b/homeassistant/components/smhi/.translations/hu.json
new file mode 100644
index 00000000000..740fc1a8179
--- /dev/null
+++ b/homeassistant/components/smhi/.translations/hu.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Sz\u00e9less\u00e9g",
+ "longitude": "Hossz\u00fas\u00e1g"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smhi/.translations/pt.json b/homeassistant/components/smhi/.translations/pt.json
new file mode 100644
index 00000000000..e814ffd5046
--- /dev/null
+++ b/homeassistant/components/smhi/.translations/pt.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "Nome j\u00e1 existe",
+ "wrong_location": "Localiza\u00e7\u00e3o apenas na Su\u00e9cia"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Nome"
+ },
+ "title": "Localiza\u00e7\u00e3o na Su\u00e9cia"
+ }
+ },
+ "title": "Servi\u00e7o meteorol\u00f3gico sueco (SMHI)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smhi/.translations/ro.json b/homeassistant/components/smhi/.translations/ro.json
index 6fe28787655..6249e49d2d7 100644
--- a/homeassistant/components/smhi/.translations/ro.json
+++ b/homeassistant/components/smhi/.translations/ro.json
@@ -1,7 +1,8 @@
{
"config": {
"error": {
- "name_exists": "Numele exist\u0103 deja"
+ "name_exists": "Numele exist\u0103 deja",
+ "wrong_location": "Loca\u021bia numai \u00een Suedia"
},
"step": {
"user": {
diff --git a/homeassistant/components/smhi/.translations/tr.json b/homeassistant/components/smhi/.translations/tr.json
new file mode 100644
index 00000000000..bb50f1e2a8d
--- /dev/null
+++ b/homeassistant/components/smhi/.translations/tr.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "Bu ad zaten var",
+ "wrong_location": "Konum sadece \u0130sve\u00e7"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Enlem",
+ "longitude": "Boylam"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py
new file mode 100644
index 00000000000..2421addfd0c
--- /dev/null
+++ b/homeassistant/components/smhi/__init__.py
@@ -0,0 +1,39 @@
+"""
+Component for the swedish weather institute weather service.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/smhi/
+"""
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import Config, HomeAssistant
+
+# Have to import for config_flow to work
+# even if they are not used here
+from .config_flow import smhi_locations # noqa: F401
+from .const import DOMAIN # noqa: F401
+
+REQUIREMENTS = ['smhi-pkg==1.0.5']
+
+DEFAULT_NAME = 'smhi'
+
+
+async def async_setup(hass: HomeAssistant, config: Config) -> bool:
+ """Set up configured smhi."""
+ # We allow setup only through config flow type of config
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant,
+ config_entry: ConfigEntry) -> bool:
+ """Set up smhi forecast as config entry."""
+ hass.async_create_task(hass.config_entries.async_forward_entry_setup(
+ config_entry, 'weather'))
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant,
+ config_entry: ConfigEntry) -> bool:
+ """Unload a config entry."""
+ await hass.config_entries.async_forward_entry_unload(
+ config_entry, 'weather')
+ return True
diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py
new file mode 100644
index 00000000000..e461c6d195d
--- /dev/null
+++ b/homeassistant/components/smhi/config_flow.py
@@ -0,0 +1,122 @@
+"""Config flow to configure smhi component.
+
+First time the user creates the configuration and
+a valid location is set in the hass configuration yaml
+it will use that location and use it as default values.
+
+Additional locations can be added in config form.
+The input location will be checked by invoking
+the API. Exception will be thrown if the location
+is not supported by the API (Swedish locations only)
+"""
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.const import (CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME)
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import aiohttp_client
+from homeassistant.util import slugify
+
+from .const import DOMAIN, HOME_LOCATION_NAME
+
+
+@callback
+def smhi_locations(hass: HomeAssistant):
+ """Return configurations of SMHI component."""
+ return set((slugify(entry.data[CONF_NAME])) for
+ entry in hass.config_entries.async_entries(DOMAIN))
+
+
+@config_entries.HANDLERS.register(DOMAIN)
+class SmhiFlowHandler(data_entry_flow.FlowHandler):
+ """Config flow for SMHI component."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ def __init__(self) -> None:
+ """Initialize SMHI forecast configuration flow."""
+ self._errors = {}
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ self._errors = {}
+
+ if user_input is not None:
+ is_ok = await self._check_location(
+ user_input[CONF_LONGITUDE],
+ user_input[CONF_LATITUDE]
+ )
+ if is_ok:
+ name = slugify(user_input[CONF_NAME])
+ if not self._name_in_configuration_exists(name):
+ return self.async_create_entry(
+ title=user_input[CONF_NAME],
+ data=user_input,
+ )
+
+ self._errors[CONF_NAME] = 'name_exists'
+ else:
+ self._errors['base'] = 'wrong_location'
+
+ # If hass config has the location set and
+ # is a valid coordinate the default location
+ # is set as default values in the form
+ if not smhi_locations(self.hass):
+ if await self._homeassistant_location_exists():
+ return await self._show_config_form(
+ name=HOME_LOCATION_NAME,
+ latitude=self.hass.config.latitude,
+ longitude=self.hass.config.longitude
+ )
+
+ return await self._show_config_form()
+
+ async def _homeassistant_location_exists(self) -> bool:
+ """Return true if default location is set and is valid."""
+ if self.hass.config.latitude != 0.0 and \
+ self.hass.config.longitude != 0.0:
+ # Return true if valid location
+ if await self._check_location(
+ self.hass.config.longitude,
+ self.hass.config.latitude):
+ return True
+ return False
+
+ def _name_in_configuration_exists(self, name: str) -> bool:
+ """Return True if name exists in configuration."""
+ if name in smhi_locations(self.hass):
+ return True
+ return False
+
+ async def _show_config_form(self,
+ name: str = None,
+ latitude: str = None,
+ longitude: str = None):
+ """Show the configuration form to edit location data."""
+ return self.async_show_form(
+ step_id='user',
+ data_schema=vol.Schema({
+ vol.Required(CONF_NAME, default=name): str,
+ vol.Required(CONF_LATITUDE, default=latitude): cv.latitude,
+ vol.Required(CONF_LONGITUDE, default=longitude): cv.longitude
+ }),
+ errors=self._errors,
+ )
+
+ async def _check_location(self, longitude: str, latitude: str) -> bool:
+ """Return true if location is ok."""
+ from smhi.smhi_lib import Smhi, SmhiForecastException
+ try:
+ session = aiohttp_client.async_get_clientsession(self.hass)
+ smhi_api = Smhi(longitude, latitude, session=session)
+
+ await smhi_api.async_get_forecast()
+
+ return True
+ except SmhiForecastException:
+ # The API will throw an exception if faulty location
+ pass
+
+ return False
diff --git a/homeassistant/components/smhi/const.py b/homeassistant/components/smhi/const.py
new file mode 100644
index 00000000000..49e0f295873
--- /dev/null
+++ b/homeassistant/components/smhi/const.py
@@ -0,0 +1,12 @@
+"""Constants in smhi component."""
+import logging
+from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
+
+HOME_LOCATION_NAME = 'Home'
+
+ATTR_SMHI_CLOUDINESS = 'cloudiness'
+DOMAIN = 'smhi'
+LOGGER = logging.getLogger('homeassistant.components.smhi')
+ENTITY_ID_SENSOR_FORMAT = WEATHER_DOMAIN + ".smhi_{}"
+ENTITY_ID_SENSOR_FORMAT_HOME = ENTITY_ID_SENSOR_FORMAT.format(
+ HOME_LOCATION_NAME)
diff --git a/homeassistant/components/smhi/strings.json b/homeassistant/components/smhi/strings.json
new file mode 100644
index 00000000000..dbf1172b7d6
--- /dev/null
+++ b/homeassistant/components/smhi/strings.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "title": "Swedish weather service (SMHI)",
+ "step": {
+ "user": {
+ "title": "Location in Sweden",
+ "data": {
+ "name": "Name",
+ "latitude": "Latitude",
+ "longitude": "Longitude"
+ }
+ }
+ },
+ "error": {
+ "name_exists": "Name already exists",
+ "wrong_location": "Location Sweden only"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/.translations/ro.json b/homeassistant/components/sonos/.translations/ro.json
new file mode 100644
index 00000000000..e442ab9504e
--- /dev/null
+++ b/homeassistant/components/sonos/.translations/ro.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Nu exist\u0103 dispozitive Sonos g\u0103site \u00een re\u021bea.",
+ "single_instance_allowed": "Este necesar\u0103 o singur\u0103 configurare a Sonos."
+ },
+ "step": {
+ "confirm": {
+ "description": "Dori\u021bi s\u0103 configura\u021bi Sonos?",
+ "title": "Sonos"
+ }
+ },
+ "title": "Sonos"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py
index 0562292acec..685402611a0 100644
--- a/homeassistant/components/switch/broadlink.py
+++ b/homeassistant/components/switch/broadlink.py
@@ -260,6 +260,7 @@ class BroadlinkSP1Switch(BroadlinkRMSwitch):
super().__init__(friendly_name, friendly_name, device, None, None)
self._command_on = 1
self._command_off = 0
+ self._load_power = None
def _sendpacket(self, packet, retry=2):
"""Send packet to device."""
@@ -288,6 +289,14 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
"""Return the polling state."""
return True
+ @property
+ def current_power_w(self):
+ """Return the current power usage in Watt."""
+ try:
+ return round(self._load_power, 2)
+ except (ValueError, TypeError):
+ return None
+
def update(self):
"""Synchronize state with switch."""
self._update()
@@ -296,6 +305,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
"""Update the state of the device."""
try:
state = self._device.check_power()
+ load_power = self._device.get_energy()
except (socket.timeout, ValueError) as error:
if retry < 1:
_LOGGER.error(error)
@@ -306,6 +316,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
if state is None and retry > 0:
return self._update(retry-1)
self._state = state
+ self._load_power = load_power
class BroadlinkMP1Slot(BroadlinkRMSwitch):
diff --git a/homeassistant/components/switch/elkm1.py b/homeassistant/components/switch/elkm1.py
new file mode 100644
index 00000000000..a838e1b948e
--- /dev/null
+++ b/homeassistant/components/switch/elkm1.py
@@ -0,0 +1,40 @@
+"""
+Support for control of ElkM1 outputs (relays).
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/switch.elkm1/
+"""
+
+
+from homeassistant.components.elkm1 import (
+ DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities)
+from homeassistant.components.switch import SwitchDevice
+
+DEPENDENCIES = [ELK_DOMAIN]
+
+
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Create the Elk-M1 switch platform."""
+ if discovery_info is None:
+ return
+ elk = hass.data[ELK_DOMAIN]['elk']
+ entities = create_elk_entities(hass, elk.outputs, 'output', ElkOutput, [])
+ async_add_entities(entities, True)
+
+
+class ElkOutput(ElkEntity, SwitchDevice):
+ """Elk output as switch."""
+
+ @property
+ def is_on(self) -> bool:
+ """Get the current output status."""
+ return self._element.output_on
+
+ async def async_turn_on(self, **kwargs):
+ """Turn on the output."""
+ self._element.turn_on(0)
+
+ async def async_turn_off(self, **kwargs):
+ """Turn off the output."""
+ self._element.turn_off()
diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py
index 05e0497155a..00388822be1 100644
--- a/homeassistant/components/switch/flux.py
+++ b/homeassistant/components/switch/flux.py
@@ -19,7 +19,7 @@ from homeassistant.components.switch import DOMAIN, SwitchDevice
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_NAME, CONF_PLATFORM, CONF_LIGHTS, CONF_MODE,
SERVICE_TURN_ON)
-from homeassistant.helpers.event import track_time_change
+from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.sun import get_astral_event_date
from homeassistant.util import slugify
from homeassistant.util.color import (
@@ -180,8 +180,10 @@ class FluxSwitch(SwitchDevice):
# Make initial update
self.flux_update()
- self.unsub_tracker = track_time_change(
- self.hass, self.flux_update, second=[0, self._interval])
+ self.unsub_tracker = track_time_interval(
+ self.hass,
+ self.flux_update,
+ datetime.timedelta(seconds=self._interval))
self.schedule_update_ha_state()
diff --git a/homeassistant/components/switch/homekit_controller.py b/homeassistant/components/switch/homekit_controller.py
index a3db6060fcf..374e59aa77b 100644
--- a/homeassistant/components/switch/homekit_controller.py
+++ b/homeassistant/components/switch/homekit_controller.py
@@ -32,8 +32,7 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice):
def update_characteristics(self, characteristics):
"""Synchronise the switch state with Home Assistant."""
- # pylint: disable=import-error
- import homekit
+ import homekit # pylint: disable=import-error
for characteristic in characteristics:
ctype = characteristic['type']
diff --git a/homeassistant/components/switch/knx.py b/homeassistant/components/switch/knx.py
index 678a8d4775f..26b9f77028d 100644
--- a/homeassistant/components/switch/knx.py
+++ b/homeassistant/components/switch/knx.py
@@ -41,7 +41,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(KNXSwitch(hass, device))
+ entities.append(KNXSwitch(device))
async_add_entities(entities)
@@ -55,17 +55,15 @@ def async_add_entities_config(hass, config, async_add_entities):
group_address=config.get(CONF_ADDRESS),
group_address_state=config.get(CONF_STATE_ADDRESS))
hass.data[DATA_KNX].xknx.devices.add(switch)
- async_add_entities([KNXSwitch(hass, switch)])
+ async_add_entities([KNXSwitch(switch)])
class KNXSwitch(SwitchDevice):
"""Representation of a KNX switch."""
- def __init__(self, hass, device):
+ def __init__(self, device):
"""Initialize of KNX switch."""
self.device = device
- self.hass = hass
- self.async_register_callbacks()
@callback
def async_register_callbacks(self):
@@ -75,6 +73,10 @@ class KNXSwitch(SwitchDevice):
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."""
diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py
index bb57f179340..ad2b963629e 100644
--- a/homeassistant/components/switch/mqtt.py
+++ b/homeassistant/components/switch/mqtt.py
@@ -14,12 +14,12 @@ from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability,
- MqttDiscoveryUpdate)
+ MqttDiscoveryUpdate, MqttEntityDeviceInfo)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import (
CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF,
- CONF_PAYLOAD_ON, CONF_ICON, STATE_ON)
+ CONF_PAYLOAD_ON, CONF_ICON, STATE_ON, CONF_DEVICE)
from homeassistant.components import mqtt, switch
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -47,6 +47,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_STATE_OFF): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
+ vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@@ -94,13 +95,15 @@ async def _async_setup_entity(hass, config, async_add_entities,
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
config.get(CONF_UNIQUE_ID),
value_template,
+ config.get(CONF_DEVICE),
discovery_hash,
)
async_add_entities([newswitch])
-class MqttSwitch(MqttAvailability, MqttDiscoveryUpdate, SwitchDevice):
+class MqttSwitch(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
+ SwitchDevice):
"""Representation of a switch that can be toggled using MQTT."""
def __init__(self, name, icon,
@@ -108,11 +111,13 @@ class MqttSwitch(MqttAvailability, MqttDiscoveryUpdate, SwitchDevice):
qos, retain, payload_on, payload_off, state_on,
state_off, optimistic, payload_available,
payload_not_available, unique_id: Optional[str],
- value_template, discovery_hash):
+ value_template, device_config: Optional[ConfigType],
+ discovery_hash):
"""Initialize the MQTT switch."""
MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
+ MqttEntityDeviceInfo.__init__(self, device_config)
self._state = False
self._name = name
self._icon = icon
diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py
index d9850c1589a..0b49cb71ba2 100644
--- a/homeassistant/components/switch/neato.py
+++ b/homeassistant/components/switch/neato.py
@@ -52,6 +52,7 @@ class NeatoConnectedSwitch(ToggleEntity):
self._state = None
self._schedule_state = None
self._clean_state = None
+ self._robot_serial = self.robot.serial
def update(self):
"""Update the states of Neato switches."""
@@ -83,6 +84,11 @@ class NeatoConnectedSwitch(ToggleEntity):
"""Return True if entity is available."""
return self._state
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._robot_serial
+
@property
def is_on(self):
"""Return true if switch is on."""
diff --git a/homeassistant/components/switch/netio.py b/homeassistant/components/switch/netio.py
index fc6086f9897..4492697406d 100644
--- a/homeassistant/components/switch/netio.py
+++ b/homeassistant/components/switch/netio.py
@@ -19,7 +19,7 @@ from homeassistant.const import (
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['pynetio==0.1.6']
+REQUIREMENTS = ['pynetio==0.1.9.1']
_LOGGER = logging.getLogger(__name__)
@@ -151,15 +151,15 @@ class NetioSwitch(SwitchDevice):
def _set(self, value):
val = list('uuuu')
- val[self.outlet - 1] = '1' if value else '0'
+ val[int(self.outlet) - 1] = '1' if value else '0'
self.netio.get('port list %s' % ''.join(val))
- self.netio.states[self.outlet - 1] = value
+ self.netio.states[int(self.outlet) - 1] = value
self.schedule_update_ha_state()
@property
def is_on(self):
"""Return the switch's status."""
- return self.netio.states[self.outlet - 1]
+ return self.netio.states[int(self.outlet) - 1]
def update(self):
"""Update the state."""
@@ -176,14 +176,14 @@ class NetioSwitch(SwitchDevice):
@property
def current_power_w(self):
"""Return actual power."""
- return self.netio.consumptions[self.outlet - 1]
+ return self.netio.consumptions[int(self.outlet) - 1]
@property
def cumulated_consumption_kwh(self):
"""Return the total enerygy consumption since start_date."""
- return self.netio.cumulated_consumptions[self.outlet - 1]
+ return self.netio.cumulated_consumptions[int(self.outlet) - 1]
@property
def start_date(self):
"""Point in time when the energy accumulation started."""
- return self.netio.start_dates[self.outlet - 1]
+ return self.netio.start_dates[int(self.outlet) - 1]
diff --git a/homeassistant/components/switch/recswitch.py b/homeassistant/components/switch/recswitch.py
new file mode 100644
index 00000000000..636c302cea1
--- /dev/null
+++ b/homeassistant/components/switch/recswitch.py
@@ -0,0 +1,101 @@
+"""
+Support for Ankuoo RecSwitch MS6126 devices.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/switch.recswitch/
+"""
+
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
+from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
+import homeassistant.helpers.config_validation as cv
+
+
+_LOGGER = logging.getLogger(__name__)
+
+REQUIREMENTS = ['pyrecswitch==1.0.2']
+
+DEFAULT_NAME = 'RecSwitch {0}'
+
+DATA_RSN = 'RSN'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_MAC): vol.All(cv.string, vol.Upper),
+ vol.Optional(CONF_NAME): cv.string,
+})
+
+
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Set up the device."""
+ from pyrecswitch import RSNetwork
+
+ host = config[CONF_HOST]
+ mac_address = config[CONF_MAC]
+ device_name = config.get(CONF_NAME)
+
+ if not hass.data.get(DATA_RSN):
+ hass.data[DATA_RSN] = RSNetwork()
+ job = hass.data[DATA_RSN].create_datagram_endpoint(loop=hass.loop)
+ hass.async_create_task(job)
+
+ device = hass.data[DATA_RSN].register_device(mac_address, host)
+ async_add_entities([RecSwitchSwitch(device, device_name, mac_address)])
+
+
+class RecSwitchSwitch(SwitchDevice):
+ """Representation of a recswitch device."""
+
+ def __init__(self, device, device_name, mac_address):
+ """Initialize a recswitch device."""
+ self.gpio_state = False
+ self.device = device
+ self.device_name = device_name
+ self.mac_address = mac_address
+ if not self.device_name:
+ self.device_name = DEFAULT_NAME.format(self.mac_address)
+
+ @property
+ def unique_id(self):
+ """Return the switch unique ID."""
+ return self.mac_address
+
+ @property
+ def name(self):
+ """Return the switch name."""
+ return self.device_name
+
+ @property
+ def is_on(self):
+ """Return true if switch is on."""
+ return self.gpio_state
+
+ async def async_turn_on(self, **kwargs):
+ """Turn on the switch."""
+ await self.async_set_gpio_status(True)
+
+ async def async_turn_off(self, **kwargs):
+ """Turn off the switch."""
+ await self.async_set_gpio_status(False)
+
+ async def async_set_gpio_status(self, status):
+ """Set the switch status."""
+ from pyrecswitch import RSNetworkError
+ try:
+ ret = await self.device.set_gpio_status(status)
+ self.gpio_state = ret.state
+ except RSNetworkError as error:
+ _LOGGER.error('Setting status to %s: %r', self.name, error)
+
+ async def async_update(self):
+ """Update the current switch status."""
+ from pyrecswitch import RSNetworkError
+ try:
+ ret = await self.device.get_gpio_status()
+ self.gpio_state = ret.state
+ except RSNetworkError as error:
+ _LOGGER.error('Reading status from %s: %r', self.name, error)
diff --git a/homeassistant/components/switch/rflink.py b/homeassistant/components/switch/rflink.py
index 2bbe3e3f03d..51bf5543584 100644
--- a/homeassistant/components/switch/rflink.py
+++ b/homeassistant/components/switch/rflink.py
@@ -6,27 +6,27 @@ https://home-assistant.io/components/switch.rflink/
"""
import logging
+import voluptuous as vol
+
from homeassistant.components.rflink import (
CONF_ALIASES, CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES,
CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, CONF_GROUP_ALIASSES,
CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS,
- DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA,
- DOMAIN, EVENT_KEY_COMMAND, SwitchableRflinkDevice, cv, remove_deprecated,
- vol)
-from homeassistant.components.switch import SwitchDevice
-from homeassistant.const import CONF_NAME, CONF_PLATFORM
-from homeassistant.helpers.deprecation import get_deprecated
+ DEVICE_DEFAULTS_SCHEMA, SwitchableRflinkDevice, remove_deprecated)
+from homeassistant.components.switch import (
+ PLATFORM_SCHEMA, SwitchDevice)
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import CONF_NAME
DEPENDENCIES = ['rflink']
_LOGGER = logging.getLogger(__name__)
-PLATFORM_SCHEMA = vol.Schema({
- vol.Required(CONF_PLATFORM): DOMAIN,
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})):
- DEVICE_DEFAULTS_SCHEMA,
- vol.Optional(CONF_DEVICES, default={}): vol.Schema({
- cv.string: {
+ DEVICE_DEFAULTS_SCHEMA,
+ vol.Optional(CONF_DEVICES, default={}): {
+ cv.string: vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ALIASES, default=[]):
vol.All(cv.ensure_list, [cv.string]),
@@ -44,50 +44,27 @@ PLATFORM_SCHEMA = vol.Schema({
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_NOGROUP_ALIASSES):
vol.All(cv.ensure_list, [cv.string]),
- },
- }),
-})
+ })
+ },
+}, extra=vol.ALLOW_EXTRA)
-def devices_from_config(domain_config, hass=None):
+def devices_from_config(domain_config):
"""Parse configuration and add Rflink switch devices."""
devices = []
for device_id, config in domain_config[CONF_DEVICES].items():
device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config)
remove_deprecated(device_config)
- device = RflinkSwitch(device_id, hass, **device_config)
+ device = RflinkSwitch(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 get_deprecated(config, CONF_ALIASES, CONF_ALIASSES):
- hass.data[DATA_ENTITY_LOOKUP][
- EVENT_KEY_COMMAND][_id].append(device)
- hass.data[DATA_ENTITY_GROUP_LOOKUP][
- EVENT_KEY_COMMAND][_id].append(device)
- # group_aliases only respond to group commands
- for _id in get_deprecated(
- config, CONF_GROUP_ALIASES, CONF_GROUP_ALIASSES):
- hass.data[DATA_ENTITY_GROUP_LOOKUP][
- EVENT_KEY_COMMAND][_id].append(device)
- # nogroup_aliases only respond to normal commands
- for _id in get_deprecated(
- config, CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES):
- hass.data[DATA_ENTITY_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 platform."""
- async_add_entities(devices_from_config(config, hass))
+ async_add_entities(devices_from_config(config))
class RflinkSwitch(SwitchableRflinkDevice, SwitchDevice):
diff --git a/homeassistant/components/switch/switchmate.py b/homeassistant/components/switch/switchmate.py
index 4955d72c5e3..7f00964cd20 100644
--- a/homeassistant/components/switch/switchmate.py
+++ b/homeassistant/components/switch/switchmate.py
@@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME, CONF_MAC
-REQUIREMENTS = ['pySwitchmate==0.4.1']
+REQUIREMENTS = ['pySwitchmate==0.4.2']
_LOGGER = logging.getLogger(__name__)
@@ -52,6 +52,11 @@ class Switchmate(SwitchDevice):
"""Return a unique, HASS-friendly identifier for this entity."""
return self._mac.replace(':', '')
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return self._device.available
+
@property
def name(self) -> str:
"""Return the name of the switch."""
diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py
index 724fcbf6075..51cea68f6b3 100644
--- a/homeassistant/components/switch/template.py
+++ b/homeassistant/components/switch/template.py
@@ -151,11 +151,11 @@ class SwitchTemplate(SwitchDevice):
async def async_turn_on(self, **kwargs):
"""Fire the on action."""
- await self._on_script.async_run()
+ await self._on_script.async_run(context=self._context)
async def async_turn_off(self, **kwargs):
"""Fire the off action."""
- await self._off_script.async_run()
+ await self._off_script.async_run(context=self._context)
async def async_update(self):
"""Update the state from the template."""
diff --git a/homeassistant/components/switch/unifi.py b/homeassistant/components/switch/unifi.py
new file mode 100644
index 00000000000..dc02068c4a8
--- /dev/null
+++ b/homeassistant/components/switch/unifi.py
@@ -0,0 +1,230 @@
+"""
+Support for devices connected to UniFi POE.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/switch.unifi/
+"""
+
+import asyncio
+import logging
+
+from datetime import timedelta
+
+import async_timeout
+
+from homeassistant.components import unifi
+from homeassistant.components.switch import SwitchDevice
+from homeassistant.components.unifi.const import (
+ CONF_CONTROLLER, CONF_SITE_ID, CONTROLLER_ID, DOMAIN)
+from homeassistant.const import CONF_HOST
+from homeassistant.core import callback
+from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
+
+DEPENDENCIES = [DOMAIN]
+SCAN_INTERVAL = timedelta(seconds=15)
+
+LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_platform(
+ hass, config, async_add_entities, discovery_info=None):
+ """Component doesn't support configuration through configuration.yaml."""
+ pass
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up switches for UniFi component.
+
+ Switches are controlling network switch ports with Poe.
+ """
+ controller_id = CONTROLLER_ID.format(
+ host=config_entry.data[CONF_CONTROLLER][CONF_HOST],
+ site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID]
+ )
+ controller = hass.data[unifi.DOMAIN][controller_id]
+ switches = {}
+
+ progress = None
+ update_progress = set()
+
+ async def request_update(object_id):
+ """Request an update."""
+ nonlocal progress
+ update_progress.add(object_id)
+
+ if progress is not None:
+ return await progress
+
+ progress = asyncio.ensure_future(update_controller())
+ result = await progress
+ progress = None
+ update_progress.clear()
+ return result
+
+ async def update_controller():
+ """Update the values of the controller."""
+ tasks = [async_update_items(
+ controller, async_add_entities, request_update,
+ switches, update_progress
+ )]
+ await asyncio.wait(tasks)
+
+ await update_controller()
+
+
+async def async_update_items(controller, async_add_entities,
+ request_controller_update, switches,
+ progress_waiting):
+ """Update POE port state from the controller."""
+ import aiounifi
+
+ @callback
+ def update_switch_state():
+ """Tell switches to reload state."""
+ for client_id, client in switches.items():
+ if client_id not in progress_waiting:
+ client.async_schedule_update_ha_state()
+
+ try:
+ with async_timeout.timeout(4):
+ await controller.api.clients.update()
+ await controller.api.devices.update()
+
+ except aiounifi.LoginRequired:
+ try:
+ with async_timeout.timeout(5):
+ await controller.api.login()
+ except (asyncio.TimeoutError, aiounifi.AiounifiException):
+ if controller.available:
+ controller.available = False
+ update_switch_state()
+ return
+
+ except (asyncio.TimeoutError, aiounifi.AiounifiException):
+ if controller.available:
+ LOGGER.error('Unable to reach controller %s', controller.host)
+ controller.available = False
+ update_switch_state()
+ return
+
+ if not controller.available:
+ LOGGER.info('Reconnected to controller %s', controller.host)
+ controller.available = True
+
+ new_switches = []
+ devices = controller.api.devices
+ for client_id in controller.api.clients:
+
+ if client_id in progress_waiting:
+ continue
+
+ if client_id in switches:
+ LOGGER.debug("Updating UniFi switch %s (%s)",
+ switches[client_id].entity_id,
+ switches[client_id].client.mac)
+ switches[client_id].async_schedule_update_ha_state()
+ continue
+
+ client = controller.api.clients[client_id]
+ # Network device with active POE
+ if not client.is_wired or client.sw_mac not in devices or \
+ not devices[client.sw_mac].ports[client.sw_port].port_poe or \
+ not devices[client.sw_mac].ports[client.sw_port].poe_enable:
+ continue
+
+ # Multiple POE-devices on same port means non UniFi POE driven switch
+ multi_clients_on_port = False
+ for client2 in controller.api.clients.values():
+ if client.mac != client2.mac and \
+ client.sw_mac == client2.sw_mac and \
+ client.sw_port == client2.sw_port:
+ multi_clients_on_port = True
+ break
+
+ if multi_clients_on_port:
+ continue
+
+ switches[client_id] = UniFiSwitch(
+ client, controller, request_controller_update)
+ new_switches.append(switches[client_id])
+ LOGGER.debug("New UniFi switch %s (%s)", client.hostname, client.mac)
+
+ if new_switches:
+ async_add_entities(new_switches)
+
+
+class UniFiSwitch(SwitchDevice):
+ """Representation of a client that uses POE."""
+
+ def __init__(self, client, controller, request_controller_update):
+ """Set up switch."""
+ self.client = client
+ self.controller = controller
+ self.poe_mode = None
+ if self.port.poe_mode != 'off':
+ self.poe_mode = self.port.poe_mode
+ self.async_request_controller_update = request_controller_update
+
+ async def async_update(self):
+ """Synchronize state with controller."""
+ await self.async_request_controller_update(self.client.mac)
+
+ @property
+ def name(self):
+ """Return the name of the switch."""
+ return self.client.hostname
+
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this switch."""
+ return 'poe-{}'.format(self.client.mac)
+
+ @property
+ def is_on(self):
+ """Return true if POE is active."""
+ return self.port.poe_mode != 'off'
+
+ @property
+ def available(self):
+ """Return if switch is available."""
+ return self.controller.available or \
+ self.client.sw_mac in self.controller.api.devices
+
+ async def async_turn_on(self, **kwargs):
+ """Enable POE for client."""
+ await self.device.async_set_port_poe_mode(
+ self.client.sw_port, self.poe_mode)
+
+ async def async_turn_off(self, **kwargs):
+ """Disable POE for client."""
+ await self.device.async_set_port_poe_mode(self.client.sw_port, 'off')
+
+ @property
+ def device_state_attributes(self):
+ """Return the device state attributes."""
+ attributes = {
+ 'power': self.port.poe_power,
+ 'received': self.client.wired_rx_bytes / 1000000,
+ 'sent': self.client.wired_tx_bytes / 1000000,
+ 'switch': self.client.sw_mac,
+ 'port': self.client.sw_port,
+ 'poe_mode': self.poe_mode
+ }
+ return attributes
+
+ @property
+ def device_info(self):
+ """Return a device description for device registry."""
+ return {
+ 'connections': {(CONNECTION_NETWORK_MAC, self.client.mac)}
+ }
+
+ @property
+ def device(self):
+ """Shortcut to the switch that client is connected to."""
+ return self.controller.api.devices[self.client.sw_mac]
+
+ @property
+ def port(self):
+ """Shortcut to the switch port that client is connected to."""
+ return self.device.ports[self.client.sw_port]
diff --git a/homeassistant/components/switch/xiaomi_aqara.py b/homeassistant/components/switch/xiaomi_aqara.py
index 17265d5dfa2..d5502c0b6fa 100644
--- a/homeassistant/components/switch/xiaomi_aqara.py
+++ b/homeassistant/components/switch/xiaomi_aqara.py
@@ -16,6 +16,7 @@ ATTR_IN_USE = 'in_use'
LOAD_POWER = 'load_power'
POWER_CONSUMED = 'power_consumed'
+ENERGY_CONSUMED = 'energy_consumed'
IN_USE = 'inuse'
@@ -57,8 +58,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
'channel_1',
False, gateway))
elif model in ['86plug', 'ctrl_86plug', 'ctrl_86plug.aq1']:
+ if 'proto' not in device or int(device['proto'][0:1]) == 1:
+ data_key = 'status'
+ else:
+ data_key = 'channel_0'
devices.append(XiaomiGenericSwitch(device, 'Wall Plug',
- 'status', True, gateway))
+ data_key, True, gateway))
add_entities(devices)
@@ -122,13 +127,17 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchDevice):
self._in_use = int(data[IN_USE])
if not self._in_use:
self._load_power = 0
- if POWER_CONSUMED in data:
- self._power_consumed = round(float(data[POWER_CONSUMED]), 2)
+
+ for key in [POWER_CONSUMED, ENERGY_CONSUMED]:
+ if key in data:
+ self._power_consumed = round(float(data[key]), 2)
+ break
+
if LOAD_POWER in data:
self._load_power = round(float(data[LOAD_POWER]), 2)
value = data.get(self._data_key)
- if value is None:
+ if value not in ['on', 'off']:
return False
state = value == 'on'
diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py
index 821de5bf647..0152615109c 100644
--- a/homeassistant/components/switch/xiaomi_miio.py
+++ b/homeassistant/components/switch/xiaomi_miio.py
@@ -10,12 +10,14 @@ import logging
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA,
- DOMAIN, )
-from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
- ATTR_ENTITY_ID, )
+from homeassistant.components.switch import (
+ DOMAIN, PLATFORM_SCHEMA, SwitchDevice)
+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__)
@@ -39,8 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
'chuangmi.plug.v3']),
})
-REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41']
-
ATTR_POWER = 'power'
ATTR_TEMPERATURE = 'temperature'
ATTR_LOAD_POWER = 'load_power'
diff --git a/homeassistant/components/switch/zwave.py b/homeassistant/components/switch/zwave.py
index 31f942bd3af..54a2a729d04 100644
--- a/homeassistant/components/switch/zwave.py
+++ b/homeassistant/components/switch/zwave.py
@@ -6,13 +6,30 @@ https://home-assistant.io/components/switch.zwave/
"""
import logging
import time
+from homeassistant.core import callback
from homeassistant.components.switch import DOMAIN, SwitchDevice
from homeassistant.components import zwave
-from homeassistant.components.zwave import workaround, async_setup_platform # noqa pylint: disable=unused-import
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Old method of setting up Z-Wave switches."""
+ pass
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Z-Wave Switch from Config Entry."""
+ @callback
+ def async_add_switch(switch):
+ """Add Z-Wave Switch."""
+ async_add_entities([switch])
+
+ async_dispatcher_connect(hass, 'zwave_new_switch', async_add_switch)
+
+
def get_device(values, **kwargs):
"""Create zwave entity device."""
return ZwaveSwitch(values)
@@ -25,8 +42,8 @@ class ZwaveSwitch(zwave.ZWaveDeviceEntity, SwitchDevice):
"""Initialize the Z-Wave switch device."""
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
self.refresh_on_update = (
- workaround.get_device_mapping(values.primary) ==
- workaround.WORKAROUND_REFRESH_NODE_ON_UPDATE)
+ zwave.workaround.get_device_mapping(values.primary) ==
+ zwave.workaround.WORKAROUND_REFRESH_NODE_ON_UPDATE)
self.last_update = time.perf_counter()
self._state = self.values.primary.data
diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py
index 29781f5052c..28bc7a1ad0d 100644
--- a/homeassistant/components/telegram_bot/__init__.py
+++ b/homeassistant/components/telegram_bot/__init__.py
@@ -21,7 +21,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import TemplateError
from homeassistant.setup import async_prepare_setup_platform
-REQUIREMENTS = ['python-telegram-bot==11.0.0']
+REQUIREMENTS = ['python-telegram-bot==11.1.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/tradfri/.translations/pt.json b/homeassistant/components/tradfri/.translations/pt.json
index 05d3cbb57fe..e89cb6ac620 100644
--- a/homeassistant/components/tradfri/.translations/pt.json
+++ b/homeassistant/components/tradfri/.translations/pt.json
@@ -18,6 +18,6 @@
"title": "Introduzir c\u00f3digo de seguran\u00e7a"
}
},
- "title": ""
+ "title": "IKEA TR\u00c5DFRI"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/ro.json b/homeassistant/components/tradfri/.translations/ro.json
new file mode 100644
index 00000000000..cea0e6d938f
--- /dev/null
+++ b/homeassistant/components/tradfri/.translations/ro.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bridge-ul este deja configurat"
+ },
+ "error": {
+ "cannot_connect": "Nu se poate conecta la gateway.",
+ "invalid_key": "Nu s-a \u00eenregistrat cu cheia furnizat\u0103. Dac\u0103 acest lucru se \u00eent\u00e2mpl\u0103 \u00een continuare, \u00eencerca\u021bi s\u0103 reporni\u021bi gateway-ul.",
+ "timeout": "Timeout la validarea codului."
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "host": "Gazd\u0103",
+ "security_code": "Cod de securitate"
+ },
+ "description": "Pute\u021bi g\u0103si codul de securitate pe spatele gateway-ului.",
+ "title": "Introduce\u021bi codul de securitate"
+ }
+ },
+ "title": "IKEA TR\u00c5DFRI"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tts/amazon_polly.py b/homeassistant/components/tts/amazon_polly.py
index d59331984b7..7b3fe4ef04e 100644
--- a/homeassistant/components/tts/amazon_polly.py
+++ b/homeassistant/components/tts/amazon_polly.py
@@ -11,7 +11,7 @@ from homeassistant.components.tts import Provider, PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['boto3==1.4.7']
+REQUIREMENTS = ['boto3==1.9.16']
CONF_REGION = 'region_name'
CONF_ACCESS_KEY_ID = 'aws_access_key_id'
@@ -87,7 +87,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def get_engine(hass, config):
"""Set up Amazon Polly speech component."""
- # pylint: disable=import-error
output_format = config.get(CONF_OUTPUT_FORMAT)
sample_rate = config.get(CONF_SAMPLE_RATE,
DEFAULT_SAMPLE_RATES[output_format])
diff --git a/homeassistant/components/twilio.py b/homeassistant/components/twilio.py
index 9f32a44ce7e..9f9767e4675 100644
--- a/homeassistant/components/twilio.py
+++ b/homeassistant/components/twilio.py
@@ -10,7 +10,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
-REQUIREMENTS = ['twilio==5.7.0']
+REQUIREMENTS = ['twilio==6.19.1']
DOMAIN = 'twilio'
@@ -51,8 +51,8 @@ class TwilioReceiveDataView(HomeAssistantView):
@callback
def post(self, request): # pylint: disable=no-self-use
"""Handle Twilio data post."""
- from twilio.twiml import Response
+ from twilio.twiml import TwiML
hass = request.app['hass']
data = yield from request.post()
hass.bus.async_fire(RECEIVED_DATA, dict(data))
- return Response().toxml()
+ return TwiML().to_xml()
diff --git a/homeassistant/components/twilio/.translations/ca.json b/homeassistant/components/twilio/.translations/ca.json
new file mode 100644
index 00000000000..3179f420ede
--- /dev/null
+++ b/homeassistant/components/twilio/.translations/ca.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "La vostra inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Twilio.",
+ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
+ },
+ "create_entry": {
+ "default": "Per enviar esdeveniments a Home Assistant, haureu de configurar [Webhooks amb Twilio] ({twilio_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - M\u00e8tode: POST \n - Tipus de contingut: application/x-www-form-urlencoded\n\nConsulteu [la documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar les dades entrants."
+ },
+ "step": {
+ "user": {
+ "description": "Esteu segur que voleu configurar Twilio?",
+ "title": "Configureu el Webhook de Twilio"
+ }
+ },
+ "title": "Twilio"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/.translations/en.json b/homeassistant/components/twilio/.translations/en.json
new file mode 100644
index 00000000000..3ee0421469c
--- /dev/null
+++ b/homeassistant/components/twilio/.translations/en.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Twilio messages.",
+ "one_instance_allowed": "Only a single instance is necessary."
+ },
+ "create_entry": {
+ "default": "To send events to Home Assistant, you will need to setup [Webhooks with Twilio]({twilio_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
+ },
+ "step": {
+ "user": {
+ "description": "Are you sure you want to set up Twilio?",
+ "title": "Set up the Twilio Webhook"
+ }
+ },
+ "title": "Twilio"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/.translations/ko.json b/homeassistant/components/twilio/.translations/ko.json
new file mode 100644
index 00000000000..028919bff90
--- /dev/null
+++ b/homeassistant/components/twilio/.translations/ko.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Twilio \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c\ud569\ub2c8\ub2e4.",
+ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4."
+ },
+ "create_entry": {
+ "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Twilio Webhook]({twilio_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/x-www-form-urlencoded\n \n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694."
+ },
+ "step": {
+ "user": {
+ "description": "Twilio \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "Twilio Webhook \uc124\uc815"
+ }
+ },
+ "title": "Twilio"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/.translations/nl.json b/homeassistant/components/twilio/.translations/nl.json
new file mode 100644
index 00000000000..a053bf372a5
--- /dev/null
+++ b/homeassistant/components/twilio/.translations/nl.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "one_instance_allowed": "Slechts \u00e9\u00e9n exemplaar is nodig."
+ },
+ "step": {
+ "user": {
+ "description": "Weet u zeker dat u Twilio wilt instellen?",
+ "title": "Stel de Twilio Webhook in"
+ }
+ },
+ "title": "Twilio"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/.translations/no.json b/homeassistant/components/twilio/.translations/no.json
new file mode 100644
index 00000000000..86e5d9051b3
--- /dev/null
+++ b/homeassistant/components/twilio/.translations/no.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Twilio"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/.translations/pl.json b/homeassistant/components/twilio/.translations/pl.json
new file mode 100644
index 00000000000..19c835c4b8c
--- /dev/null
+++ b/homeassistant/components/twilio/.translations/pl.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty Twilio.",
+ "one_instance_allowed": "Wymagana jest tylko jedna instancja."
+ },
+ "create_entry": {
+ "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Twilio Webhook]({twilio_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/x-www-form-urlencoded \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane."
+ },
+ "step": {
+ "user": {
+ "description": "Czy chcesz skonfigurowa\u0107 Twilio?",
+ "title": "Konfiguracja Twilio Webhook"
+ }
+ },
+ "title": "Twilio"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/.translations/ru.json b/homeassistant/components/twilio/.translations/ru.json
new file mode 100644
index 00000000000..e758a47064e
--- /dev/null
+++ b/homeassistant/components/twilio/.translations/ru.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Twilio.",
+ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ },
+ "create_entry": {
+ "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Twilio]({twilio_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445."
+ },
+ "step": {
+ "user": {
+ "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Twilio?",
+ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Twilio Webhook"
+ }
+ },
+ "title": "Twilio"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/.translations/sl.json b/homeassistant/components/twilio/.translations/sl.json
new file mode 100644
index 00000000000..0321cb05452
--- /dev/null
+++ b/homeassistant/components/twilio/.translations/sl.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": " \u010ce \u017eelite prejemati sporo\u010dila Twilio, mora biti Home Assistent dostopen prek interneta.",
+ "one_instance_allowed": "Potrebna je samo ena instanca."
+ },
+ "create_entry": {
+ "default": "Za po\u0161iljanje dogodkov Home Assistent-u, boste morali nastaviti [Webhooks z Twilio]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) o tem, kako nastavite avtomatizacijo za obravnavo dohodnih podatkov."
+ },
+ "step": {
+ "user": {
+ "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti Twilio?",
+ "title": "Nastavite Twilio Webhook"
+ }
+ },
+ "title": "Twilio"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/.translations/zh-Hant.json b/homeassistant/components/twilio/.translations/zh-Hant.json
new file mode 100644
index 00000000000..2e85ef7b2de
--- /dev/null
+++ b/homeassistant/components/twilio/.translations/zh-Hant.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Twilio \u8a0a\u606f\u3002",
+ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002"
+ },
+ "create_entry": {
+ "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8a2d\u5b9a [Webhooks with Twilio]({twilio_url})\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002"
+ },
+ "step": {
+ "user": {
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a Twilio\uff1f",
+ "title": "\u8a2d\u5b9a Twilio Webhook"
+ }
+ },
+ "title": "Twilio"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json
new file mode 100644
index 00000000000..4f570fe1386
--- /dev/null
+++ b/homeassistant/components/unifi/.translations/es.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El sitio del controlador ya est\u00e1 configurado",
+ "user_privilege": "El usuario debe ser administrador"
+ },
+ "error": {
+ "faulty_credentials": "Credenciales de usuario incorrectas",
+ "service_unavailable": "Servicio No disponible"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Contrase\u00f1a",
+ "port": "Puerto",
+ "site": "ID del sitio",
+ "username": "Nombre de usuario",
+ "verify_ssl": "Controlador usando el certificado adecuado"
+ },
+ "title": "Configurar el controlador UniFi"
+ }
+ },
+ "title": "Controlador UniFi"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json
new file mode 100644
index 00000000000..68e90811a3e
--- /dev/null
+++ b/homeassistant/components/unifi/.translations/fr.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "user_privilege": "L'utilisateur doit \u00eatre administrateur"
+ },
+ "error": {
+ "faulty_credentials": "Mauvaises informations d'identification de l'utilisateur",
+ "service_unavailable": "Aucun service disponible"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "H\u00f4te",
+ "password": "Mot de passe",
+ "port": "Port",
+ "site": "ID du site",
+ "username": "Nom d'utilisateur"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/.translations/hu.json b/homeassistant/components/unifi/.translations/hu.json
index f5827c47353..06104c6ed6c 100644
--- a/homeassistant/components/unifi/.translations/hu.json
+++ b/homeassistant/components/unifi/.translations/hu.json
@@ -1,10 +1,18 @@
{
"config": {
+ "abort": {
+ "user_privilege": "A felhaszn\u00e1l\u00f3nak rendszergazd\u00e1nak kell lennie"
+ },
+ "error": {
+ "faulty_credentials": "Rossz felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok",
+ "service_unavailable": "Nincs el\u00e9rhet\u0151 szolg\u00e1ltat\u00e1s"
+ },
"step": {
"user": {
"data": {
"password": "Jelsz\u00f3",
- "port": "Port"
+ "port": "Port",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
}
}
}
diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json
new file mode 100644
index 00000000000..7a1eea546a2
--- /dev/null
+++ b/homeassistant/components/unifi/.translations/nl.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Controller site is al geconfigureerd",
+ "user_privilege": "Gebruiker moet beheerder zijn"
+ },
+ "error": {
+ "faulty_credentials": "Foutieve gebruikersgegevens",
+ "service_unavailable": "Geen service beschikbaar"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Wachtwoord",
+ "port": "Poort",
+ "site": "Site ID",
+ "username": "Gebruikersnaam",
+ "verify_ssl": "Controller gebruik van het juiste certificaat"
+ },
+ "title": "Stel de UniFi-controller in"
+ }
+ },
+ "title": "UniFi-controller"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json
new file mode 100644
index 00000000000..7e9251dc026
--- /dev/null
+++ b/homeassistant/components/unifi/.translations/no.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passord",
+ "port": "Port",
+ "username": "Brukernavn"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json
index f2f8082ac76..5382adcbf7d 100644
--- a/homeassistant/components/unifi/.translations/pl.json
+++ b/homeassistant/components/unifi/.translations/pl.json
@@ -18,7 +18,7 @@
"username": "Nazwa u\u017cytkownika",
"verify_ssl": "Kontroler u\u017cywa prawid\u0142owego certyfikatu"
},
- "title": "Skonfiguruj kontroler UniFi"
+ "title": "Konfiguracja kontrolera UniFi"
}
},
"title": "Kontroler UniFi"
diff --git a/homeassistant/components/unifi/.translations/pt.json b/homeassistant/components/unifi/.translations/pt.json
new file mode 100644
index 00000000000..6730a3d258e
--- /dev/null
+++ b/homeassistant/components/unifi/.translations/pt.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "O site do controlador j\u00e1 se encontra configurado",
+ "user_privilege": "Utilizador tem que ser administrador"
+ },
+ "error": {
+ "faulty_credentials": "Credenciais do utilizador erradas",
+ "service_unavailable": "Nenhum servi\u00e7o dispon\u00edvel"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Servidor",
+ "password": "Palavra-passe",
+ "port": "Porto",
+ "site": "Site ID",
+ "username": "Nome do utilizador",
+ "verify_ssl": "Controlador com certificados adequados"
+ },
+ "title": "Configurar o controlador UniFi"
+ }
+ },
+ "title": "Controlador UniFi"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/.translations/ro.json b/homeassistant/components/unifi/.translations/ro.json
new file mode 100644
index 00000000000..99b1ac57e0b
--- /dev/null
+++ b/homeassistant/components/unifi/.translations/ro.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "user_privilege": "Utilizatorul trebuie s\u0103 fie administrator"
+ },
+ "error": {
+ "faulty_credentials": "Credentiale utilizator invalide",
+ "service_unavailable": "Nici un serviciu disponibil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Gazd\u0103",
+ "password": "Parol\u0103",
+ "port": "Port",
+ "username": "Nume de utilizator",
+ "verify_ssl": "Controler utiliz\u00e2nd certificatul adecvat"
+ },
+ "title": "Configura\u021bi un controler UniFi"
+ }
+ },
+ "title": "Controler UniFi"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/.translations/tr.json b/homeassistant/components/unifi/.translations/tr.json
new file mode 100644
index 00000000000..667a5e676fb
--- /dev/null
+++ b/homeassistant/components/unifi/.translations/tr.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "Parola",
+ "username": "Kullan\u0131c\u0131 ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py
new file mode 100644
index 00000000000..26b60aecf42
--- /dev/null
+++ b/homeassistant/components/unifi/__init__.py
@@ -0,0 +1,186 @@
+"""
+Support for devices connected to UniFi POE.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/unifi/
+"""
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import (
+ CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL)
+from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
+
+from .const import (CONF_CONTROLLER, CONF_POE_CONTROL, CONF_SITE_ID,
+ CONTROLLER_ID, DOMAIN, LOGGER)
+from .controller import UniFiController, get_controller
+from .errors import (
+ AlreadyConfigured, AuthenticationRequired, CannotConnect, UserLevel)
+
+DEFAULT_PORT = 8443
+DEFAULT_SITE_ID = 'default'
+DEFAULT_VERIFY_SSL = False
+
+REQUIREMENTS = ['aiounifi==3']
+
+
+async def async_setup(hass, config):
+ """Component doesn't support configuration through configuration.yaml."""
+ return True
+
+
+async def async_setup_entry(hass, config_entry):
+ """Set up the UniFi component."""
+ controller = UniFiController(hass, config_entry)
+
+ if DOMAIN not in hass.data:
+ hass.data[DOMAIN] = {}
+ controller_id = CONTROLLER_ID.format(
+ host=config_entry.data[CONF_CONTROLLER][CONF_HOST],
+ site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID]
+ )
+
+ if not await controller.async_setup():
+ return False
+
+ hass.data[DOMAIN][controller_id] = controller
+
+ if controller.mac is None:
+ return True
+
+ device_registry = await \
+ hass.helpers.device_registry.async_get_registry()
+ device_registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(CONNECTION_NETWORK_MAC, controller.mac)},
+ manufacturer='Ubiquiti',
+ model="UniFi Controller",
+ name="UniFi Controller",
+ # sw_version=config.raw['swversion'],
+ )
+
+ return True
+
+
+async def async_unload_entry(hass, config_entry):
+ """Unload a config entry."""
+ controller_id = CONTROLLER_ID.format(
+ host=config_entry.data[CONF_CONTROLLER][CONF_HOST],
+ site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID]
+ )
+ controller = hass.data[DOMAIN].pop(controller_id)
+ return await controller.async_reset()
+
+
+@config_entries.HANDLERS.register(DOMAIN)
+class UnifiFlowHandler(config_entries.ConfigFlow):
+ """Handle a UniFi config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ def __init__(self):
+ """Initialize the UniFi flow."""
+ self.config = None
+ self.desc = None
+ self.sites = None
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ errors = {}
+
+ if user_input is not None:
+
+ try:
+ self.config = {
+ CONF_HOST: user_input[CONF_HOST],
+ CONF_USERNAME: user_input[CONF_USERNAME],
+ CONF_PASSWORD: user_input[CONF_PASSWORD],
+ CONF_PORT: user_input.get(CONF_PORT),
+ CONF_VERIFY_SSL: user_input.get(CONF_VERIFY_SSL),
+ CONF_SITE_ID: DEFAULT_SITE_ID,
+ }
+ controller = await get_controller(self.hass, **self.config)
+
+ self.sites = await controller.sites()
+
+ return await self.async_step_site()
+
+ except AuthenticationRequired:
+ errors['base'] = 'faulty_credentials'
+
+ except CannotConnect:
+ errors['base'] = 'service_unavailable'
+
+ except Exception: # pylint: disable=broad-except
+ LOGGER.error(
+ 'Unknown error connecting with UniFi Controller at %s',
+ user_input[CONF_HOST])
+ return self.async_abort(reason='unknown')
+
+ return self.async_show_form(
+ step_id='user',
+ data_schema=vol.Schema({
+ vol.Required(CONF_HOST): str,
+ vol.Required(CONF_USERNAME): str,
+ vol.Required(CONF_PASSWORD): str,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
+ vol.Optional(
+ CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool,
+ }),
+ errors=errors,
+ )
+
+ async def async_step_site(self, user_input=None):
+ """Select site to control."""
+ errors = {}
+
+ if user_input is not None:
+
+ try:
+ desc = user_input.get(CONF_SITE_ID, self.desc)
+ for site in self.sites.values():
+ if desc == site['desc']:
+ if site['role'] != 'admin':
+ raise UserLevel
+ self.config[CONF_SITE_ID] = site['name']
+ break
+
+ for entry in self._async_current_entries():
+ controller = entry.data[CONF_CONTROLLER]
+ if controller[CONF_HOST] == self.config[CONF_HOST] and \
+ controller[CONF_SITE_ID] == self.config[CONF_SITE_ID]:
+ raise AlreadyConfigured
+
+ data = {
+ CONF_CONTROLLER: self.config,
+ CONF_POE_CONTROL: True
+ }
+
+ return self.async_create_entry(
+ title=desc,
+ data=data
+ )
+
+ except AlreadyConfigured:
+ return self.async_abort(reason='already_configured')
+
+ except UserLevel:
+ return self.async_abort(reason='user_privilege')
+
+ if len(self.sites) == 1:
+ self.desc = next(iter(self.sites.values()))['desc']
+ return await self.async_step_site(user_input={})
+
+ sites = []
+ for site in self.sites.values():
+ sites.append(site['desc'])
+
+ return self.async_show_form(
+ step_id='site',
+ data_schema=vol.Schema({
+ vol.Required(CONF_SITE_ID): vol.In(sites)
+ }),
+ errors=errors,
+ )
diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py
new file mode 100644
index 00000000000..7250feec799
--- /dev/null
+++ b/homeassistant/components/unifi/const.py
@@ -0,0 +1,12 @@
+"""Constants for the UniFi component."""
+
+import logging
+
+LOGGER = logging.getLogger('homeassistant.components.unifi')
+DOMAIN = 'unifi'
+
+CONTROLLER_ID = '{host}-{site}'
+
+CONF_CONTROLLER = 'controller'
+CONF_POE_CONTROL = 'poe_control'
+CONF_SITE_ID = 'site'
diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py
new file mode 100644
index 00000000000..9e21956536f
--- /dev/null
+++ b/homeassistant/components/unifi/controller.py
@@ -0,0 +1,131 @@
+"""UniFi Controller abstraction."""
+
+import asyncio
+import async_timeout
+
+from aiohttp import CookieJar
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_HOST
+from homeassistant.helpers import aiohttp_client
+
+from .const import CONF_CONTROLLER, CONF_POE_CONTROL, LOGGER
+from .errors import AuthenticationRequired, CannotConnect
+
+
+class UniFiController:
+ """Manages a single UniFi Controller."""
+
+ def __init__(self, hass, config_entry):
+ """Initialize the system."""
+ self.hass = hass
+ self.config_entry = config_entry
+ self.available = True
+ self.api = None
+ self.progress = None
+ self._cancel_retry_setup = None
+
+ @property
+ def host(self):
+ """Return the host of this controller."""
+ return self.config_entry.data[CONF_CONTROLLER][CONF_HOST]
+
+ @property
+ def mac(self):
+ """Return the mac address of this controller."""
+ for client in self.api.clients.values():
+ if self.host == client.ip:
+ return client.mac
+ return None
+
+ async def async_setup(self, tries=0):
+ """Set up a UniFi controller."""
+ hass = self.hass
+
+ try:
+ self.api = await get_controller(
+ self.hass, **self.config_entry.data[CONF_CONTROLLER])
+ await self.api.initialize()
+
+ except CannotConnect:
+ retry_delay = 2 ** (tries + 1)
+ LOGGER.error("Error connecting to the UniFi controller. Retrying "
+ "in %d seconds", retry_delay)
+
+ async def retry_setup(_now):
+ """Retry setup."""
+ if await self.async_setup(tries + 1):
+ # This feels hacky, we should find a better way to do this
+ self.config_entry.state = config_entries.ENTRY_STATE_LOADED
+
+ self._cancel_retry_setup = hass.helpers.event.async_call_later(
+ retry_delay, retry_setup)
+
+ return False
+
+ except Exception: # pylint: disable=broad-except
+ LOGGER.error(
+ 'Unknown error connecting with UniFi controller.')
+ return False
+
+ if self.config_entry.data[CONF_POE_CONTROL]:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(
+ self.config_entry, 'switch'))
+
+ return True
+
+ async def async_reset(self):
+ """Reset this controller to default state.
+
+ Will cancel any scheduled setup retry and will unload
+ the config entry.
+ """
+ # If we have a retry scheduled, we were never setup.
+ if self._cancel_retry_setup is not None:
+ self._cancel_retry_setup()
+ self._cancel_retry_setup = None
+ return True
+
+ # If the authentication was wrong.
+ if self.api is None:
+ return True
+
+ if self.config_entry.data[CONF_POE_CONTROL]:
+ return await self.hass.config_entries.async_forward_entry_unload(
+ self.config_entry, 'switch')
+ return True
+
+
+async def get_controller(
+ hass, host, username, password, port, site, verify_ssl):
+ """Create a controller object and verify authentication."""
+ import aiounifi
+
+ if verify_ssl:
+ session = aiohttp_client.async_get_clientsession(hass)
+ else:
+ session = aiohttp_client.async_create_clientsession(
+ hass, verify_ssl=verify_ssl, cookie_jar=CookieJar(unsafe=True))
+
+ controller = aiounifi.Controller(
+ host, username=username, password=password, port=port, site=site,
+ websession=session
+ )
+
+ try:
+ with async_timeout.timeout(5):
+ await controller.login()
+ return controller
+
+ except aiounifi.Unauthorized:
+ LOGGER.warning("Connected to UniFi at %s but not registered.", host)
+ raise AuthenticationRequired
+
+ except (asyncio.TimeoutError, aiounifi.RequestError):
+ LOGGER.error("Error connecting to the UniFi controller at %s", host)
+ raise CannotConnect
+
+ except aiounifi.AiounifiException:
+ LOGGER.exception('Unknown UniFi communication error occurred')
+ raise AuthenticationRequired
diff --git a/homeassistant/components/unifi/errors.py b/homeassistant/components/unifi/errors.py
new file mode 100644
index 00000000000..c90c4956312
--- /dev/null
+++ b/homeassistant/components/unifi/errors.py
@@ -0,0 +1,26 @@
+"""Errors for the UniFi component."""
+from homeassistant.exceptions import HomeAssistantError
+
+
+class UnifiException(HomeAssistantError):
+ """Base class for UniFi exceptions."""
+
+
+class AlreadyConfigured(UnifiException):
+ """Controller is already configured."""
+
+
+class AuthenticationRequired(UnifiException):
+ """Unknown error occurred."""
+
+
+class CannotConnect(UnifiException):
+ """Unable to connect to the controller."""
+
+
+class LoginRequired(UnifiException):
+ """Component got logged out."""
+
+
+class UserLevel(UnifiException):
+ """User level too low."""
diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json
new file mode 100644
index 00000000000..938ac058d22
--- /dev/null
+++ b/homeassistant/components/unifi/strings.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "title": "UniFi Controller",
+ "step": {
+ "user": {
+ "title": "Set up UniFi Controller",
+ "data": {
+ "host": "Host",
+ "username": "User name",
+ "password": "Password",
+ "port": "Port",
+ "site": "Site ID",
+ "verify_ssl": "Controller using proper certificate"
+ }
+ }
+ },
+ "error": {
+ "faulty_credentials": "Bad user credentials",
+ "service_unavailable": "No service available"
+ },
+ "abort": {
+ "already_configured": "Controller site is already configured",
+ "user_privilege": "User needs to be administrator"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py
index 4e64e3be2e6..6f7b75cf549 100644
--- a/homeassistant/components/updater.py
+++ b/homeassistant/components/updater.py
@@ -4,9 +4,9 @@ Support to check for available updates.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/updater/
"""
-# pylint: disable=no-name-in-module, import-error
import asyncio
from datetime import timedelta
+# pylint: disable=import-error,no-name-in-module
from distutils.version import StrictVersion
import json
import logging
diff --git a/homeassistant/components/upnp/.translations/es.json b/homeassistant/components/upnp/.translations/es.json
new file mode 100644
index 00000000000..e4cabf4cd50
--- /dev/null
+++ b/homeassistant/components/upnp/.translations/es.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "UPnP / IGD ya est\u00e1 configurado",
+ "no_devices_discovered": "No se descubrieron UPnP / IGDs",
+ "no_sensors_or_port_mapping": "Habilitar al menos sensores o mapeo de puertos"
+ },
+ "step": {
+ "init": {
+ "title": "UPnP / IGD"
+ },
+ "user": {
+ "data": {
+ "enable_port_mapping": "Habilitar la asignaci\u00f3n de puertos para Home Assistant",
+ "enable_sensors": "A\u00f1adir sensores de tr\u00e1fico",
+ "igd": "UPnP / IGD"
+ },
+ "title": "Opciones de configuraci\u00f3n para UPnP/IGD"
+ }
+ },
+ "title": "UPnP / IGD"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upnp/.translations/hu.json b/homeassistant/components/upnp/.translations/hu.json
new file mode 100644
index 00000000000..a2bf78a7f3e
--- /dev/null
+++ b/homeassistant/components/upnp/.translations/hu.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "step": {
+ "init": {
+ "title": "UPnP/IGD"
+ },
+ "user": {
+ "data": {
+ "igd": "UPnP/IGD"
+ },
+ "title": "Az UPnP/IGD be\u00e1ll\u00edt\u00e1si lehet\u0151s\u00e9gei"
+ }
+ },
+ "title": "UPnP/IGD"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upnp/.translations/pt.json b/homeassistant/components/upnp/.translations/pt.json
new file mode 100644
index 00000000000..899a5def479
--- /dev/null
+++ b/homeassistant/components/upnp/.translations/pt.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "UPnP/IGD j\u00e1 est\u00e1 configurado",
+ "no_devices_discovered": "Nenhum UPnP/IGDs descoberto",
+ "no_sensors_or_port_mapping": "Ative pelo menos os sensores ou o mapeamento de porta"
+ },
+ "error": {
+ "one": "um",
+ "other": "v\u00e1rios"
+ },
+ "step": {
+ "init": {
+ "title": "UPnP/IGD"
+ },
+ "user": {
+ "data": {
+ "enable_port_mapping": "Ativar o mapeamento de porta para o Home Assistant",
+ "enable_sensors": "Adicionar sensores de tr\u00e1fego",
+ "igd": "UPnP/IGD"
+ },
+ "title": "Op\u00e7\u00f5es de configura\u00e7\u00e3o para o UPnP/IGD"
+ }
+ },
+ "title": "UPnP/IGD"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upnp/.translations/tr.json b/homeassistant/components/upnp/.translations/tr.json
new file mode 100644
index 00000000000..91503c17a07
--- /dev/null
+++ b/homeassistant/components/upnp/.translations/tr.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "enable_sensors": "Trafik sens\u00f6rleri ekleyin"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py
index c496caba948..07c8d5f748e 100644
--- a/homeassistant/components/upnp/__init__.py
+++ b/homeassistant/components/upnp/__init__.py
@@ -30,7 +30,7 @@ from .config_flow import ensure_domain_data
from .device import Device
-REQUIREMENTS = ['async-upnp-client==0.12.6']
+REQUIREMENTS = ['async-upnp-client==0.12.7']
DEPENDENCIES = ['http']
NOTIFICATION_ID = 'upnp_notification'
@@ -43,8 +43,8 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string),
vol.Optional(CONF_PORTS):
vol.Schema({
- vol.Any(CONF_HASS, cv.positive_int):
- vol.Any(CONF_HASS, cv.positive_int)
+ vol.Any(CONF_HASS, cv.port):
+ vol.Any(CONF_HASS, cv.port)
})
}),
}, extra=vol.ALLOW_EXTRA)
diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py
index 29db94de762..83a4ad7c58d 100644
--- a/homeassistant/components/vacuum/neato.py
+++ b/homeassistant/components/vacuum/neato.py
@@ -64,6 +64,9 @@ class NeatoConnectedVacuum(StateVacuumDevice):
self.clean_battery_end = None
self.clean_suspension_charge_count = None
self.clean_suspension_time = None
+ self._available = False
+ self._battery_level = None
+ self._robot_serial = self.robot.serial
def update(self):
"""Update the states of Neato Vacuums."""
@@ -71,12 +74,12 @@ class NeatoConnectedVacuum(StateVacuumDevice):
self.neato.update_robots()
try:
self._state = self.robot.state
+ self._available = True
except (requests.exceptions.ConnectionError,
requests.exceptions.HTTPError) as ex:
_LOGGER.warning("Neato connection error: %s", ex)
self._state = None
- self._clean_state = STATE_ERROR
- self._status_state = 'Robot Offline'
+ self._available = False
return
_LOGGER.debug('self._state=%s', self._state)
if self._state['state'] == 1:
@@ -105,27 +108,30 @@ class NeatoConnectedVacuum(StateVacuumDevice):
self._clean_state = STATE_ERROR
self._status_state = ERRORS.get(self._state['error'])
- if not self._mapdata.get(self.robot.serial, {}).get('maps', []):
+ if not self._mapdata.get(self._robot_serial, {}).get('maps', []):
return
self.clean_time_start = (
- (self._mapdata[self.robot.serial]['maps'][0]['start_at']
+ (self._mapdata[self._robot_serial]['maps'][0]['start_at']
.strip('Z'))
.replace('T', ' '))
self.clean_time_stop = (
- (self._mapdata[self.robot.serial]['maps'][0]['end_at'].strip('Z'))
+ (self._mapdata[self._robot_serial]['maps'][0]['end_at'].strip('Z'))
.replace('T', ' '))
self.clean_area = (
- self._mapdata[self.robot.serial]['maps'][0]['cleaned_area'])
+ self._mapdata[self._robot_serial]['maps'][0]['cleaned_area'])
self.clean_suspension_charge_count = (
- self._mapdata[self.robot.serial]['maps'][0]
+ self._mapdata[self._robot_serial]['maps'][0]
['suspended_cleaning_charging_count'])
self.clean_suspension_time = (
- self._mapdata[self.robot.serial]['maps'][0]
+ self._mapdata[self._robot_serial]['maps'][0]
['time_in_suspended_cleaning'])
self.clean_battery_start = (
- self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_start'])
+ self._mapdata[self._robot_serial]['maps'][0]['run_charge_at_start']
+ )
self.clean_battery_end = (
- self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_end'])
+ self._mapdata[self._robot_serial]['maps'][0]['run_charge_at_end'])
+
+ self._battery_level = self._state['details']['charge']
@property
def name(self):
@@ -140,13 +146,23 @@ class NeatoConnectedVacuum(StateVacuumDevice):
@property
def battery_level(self):
"""Return the battery level of the vacuum cleaner."""
- return self._state['details']['charge']
+ return self._battery_level
+
+ @property
+ def available(self):
+ """Return if the robot is available."""
+ return self._available
@property
def state(self):
"""Return the status of the vacuum cleaner."""
return self._clean_state
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._robot_serial
+
@property
def device_state_attributes(self):
"""Return the state attributes of the vacuum cleaner."""
diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py
index d2da4f3b6ac..d1d45f5ecd2 100644
--- a/homeassistant/components/vacuum/xiaomi_miio.py
+++ b/homeassistant/components/vacuum/xiaomi_miio.py
@@ -21,7 +21,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41']
+REQUIREMENTS = ['python-miio==0.4.2', 'construct==2.9.45']
_LOGGER = logging.getLogger(__name__)
@@ -179,6 +179,10 @@ class MiroboVacuum(StateVacuumDevice):
def state(self):
"""Return the status of the vacuum cleaner."""
if self.vacuum_state is not None:
+ # The vacuum reverts back to an idle state after erroring out.
+ # We want to keep returning an error until it has been cleared.
+ if self.vacuum_state.got_error:
+ return STATE_ERROR
try:
return STATE_CODE_TO_STATE[int(self.vacuum_state.state_code)]
except KeyError:
diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py
index 5bc6260c0a7..127cd008a3a 100644
--- a/homeassistant/components/vera.py
+++ b/homeassistant/components/vera.py
@@ -19,7 +19,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, CONF_LIGHTS, CONF_EXCLUDE)
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['pyvera==0.2.44']
+REQUIREMENTS = ['pyvera==0.2.45']
_LOGGER = logging.getLogger(__name__)
@@ -195,3 +195,11 @@ class VeraDevice(Entity):
attr['Vera Device Id'] = self.vera_device.vera_device_id
return attr
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique ID.
+
+ The Vera assigns a unique and immutable ID number to each device.
+ """
+ return str(self.vera_device.vera_device_id)
diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py
index 016547697b9..2c8c34fa67d 100644
--- a/homeassistant/components/verisure.py
+++ b/homeassistant/components/verisure.py
@@ -16,7 +16,7 @@ from homeassistant.helpers import discovery
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['vsure==1.3.7', 'jsonpath==0.75']
+REQUIREMENTS = ['vsure==1.5.0', 'jsonpath==0.75']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py
new file mode 100644
index 00000000000..92dbebc4421
--- /dev/null
+++ b/homeassistant/components/water_heater/__init__.py
@@ -0,0 +1,263 @@
+"""
+Provides functionality to interact with water heater devices.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/water_heater/
+"""
+from datetime import timedelta
+import logging
+import functools as ft
+
+import voluptuous as vol
+
+from homeassistant.helpers.temperature import display_temp as show_temp
+from homeassistant.util.temperature import convert as convert_temperature
+from homeassistant.helpers.entity_component import EntityComponent
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import (
+ ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF,
+ STATE_ON, STATE_OFF, TEMP_CELSIUS, PRECISION_WHOLE,
+ PRECISION_TENTHS, TEMP_FAHRENHEIT)
+
+DEFAULT_MIN_TEMP = 110
+DEFAULT_MAX_TEMP = 140
+
+DOMAIN = 'water_heater'
+
+ENTITY_ID_FORMAT = DOMAIN + '.{}'
+SCAN_INTERVAL = timedelta(seconds=60)
+
+SERVICE_SET_AWAY_MODE = 'set_away_mode'
+SERVICE_SET_TEMPERATURE = 'set_temperature'
+SERVICE_SET_OPERATION_MODE = 'set_operation_mode'
+
+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_OPERATION_MODE = 2
+SUPPORT_AWAY_MODE = 4
+
+ATTR_MAX_TEMP = 'max_temp'
+ATTR_MIN_TEMP = 'min_temp'
+ATTR_AWAY_MODE = 'away_mode'
+ATTR_OPERATION_MODE = 'operation_mode'
+ATTR_OPERATION_LIST = 'operation_list'
+
+CONVERTIBLE_ATTRIBUTE = [
+ ATTR_TEMPERATURE,
+]
+
+_LOGGER = logging.getLogger(__name__)
+
+ON_OFF_SERVICE_SCHEMA = vol.Schema({
+ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
+})
+
+SET_AWAY_MODE_SCHEMA = vol.Schema({
+ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
+ vol.Required(ATTR_AWAY_MODE): cv.boolean,
+})
+SET_TEMPERATURE_SCHEMA = vol.Schema(vol.All(
+ {
+ vol.Required(ATTR_TEMPERATURE, 'temperature'): vol.Coerce(float),
+ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
+ vol.Optional(ATTR_OPERATION_MODE): cv.string,
+ }
+))
+SET_OPERATION_MODE_SCHEMA = vol.Schema({
+ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
+ vol.Required(ATTR_OPERATION_MODE): cv.string,
+})
+
+
+async def async_setup(hass, config):
+ """Set up water_heater devices."""
+ component = hass.data[DOMAIN] = \
+ EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
+ await component.async_setup(config)
+
+ component.async_register_entity_service(
+ SERVICE_SET_AWAY_MODE, SET_AWAY_MODE_SCHEMA,
+ async_service_away_mode
+ )
+ component.async_register_entity_service(
+ SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA,
+ async_service_temperature_set
+ )
+ component.async_register_entity_service(
+ SERVICE_SET_OPERATION_MODE, SET_OPERATION_MODE_SCHEMA,
+ 'async_set_operation_mode'
+ )
+ component.async_register_entity_service(
+ SERVICE_TURN_OFF, ON_OFF_SERVICE_SCHEMA,
+ 'async_turn_off'
+ )
+ component.async_register_entity_service(
+ SERVICE_TURN_ON, ON_OFF_SERVICE_SCHEMA,
+ 'async_turn_on'
+ )
+
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Set up a config entry."""
+ return await hass.data[DOMAIN].async_setup_entry(entry)
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a config entry."""
+ return await hass.data[DOMAIN].async_unload_entry(entry)
+
+
+class WaterHeaterDevice(Entity):
+ """Representation of a water_heater device."""
+
+ @property
+ def state(self):
+ """Return the current state."""
+ return self.current_operation
+
+ @property
+ def precision(self):
+ """Return the precision of the system."""
+ if self.hass.config.units.temperature_unit == TEMP_CELSIUS:
+ return PRECISION_TENTHS
+ return PRECISION_WHOLE
+
+ @property
+ def state_attributes(self):
+ """Return the optional state attributes."""
+ data = {
+ ATTR_MIN_TEMP: show_temp(
+ self.hass, self.min_temp, self.temperature_unit,
+ self.precision),
+ ATTR_MAX_TEMP: show_temp(
+ self.hass, self.max_temp, self.temperature_unit,
+ self.precision),
+ ATTR_TEMPERATURE: show_temp(
+ self.hass, self.target_temperature, self.temperature_unit,
+ self.precision),
+ }
+
+ supported_features = self.supported_features
+
+ if supported_features & SUPPORT_OPERATION_MODE:
+ data[ATTR_OPERATION_MODE] = self.current_operation
+ if self.operation_list:
+ data[ATTR_OPERATION_LIST] = self.operation_list
+
+ if supported_features & SUPPORT_AWAY_MODE:
+ is_away = self.is_away_mode_on
+ data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
+
+ return data
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement used by the platform."""
+ raise NotImplementedError
+
+ @property
+ def current_operation(self):
+ """Return current operation ie. eco, electric, performance, ..."""
+ return None
+
+ @property
+ def operation_list(self):
+ """Return the list of available operation modes."""
+ return None
+
+ @property
+ def target_temperature(self):
+ """Return the temperature we try to reach."""
+ return None
+
+ @property
+ def is_away_mode_on(self):
+ """Return true if away mode is on."""
+ return None
+
+ def set_temperature(self, **kwargs):
+ """Set new target temperature."""
+ raise NotImplementedError()
+
+ async def async_set_temperature(self, **kwargs):
+ """Set new target temperature."""
+ await self.hass.async_add_executor_job(
+ ft.partial(self.set_temperature, **kwargs))
+
+ def set_operation_mode(self, operation_mode):
+ """Set new target operation mode."""
+ raise NotImplementedError()
+
+ async def async_set_operation_mode(self, operation_mode):
+ """Set new target operation mode."""
+ await self.hass.async_add_executor_job(self.set_operation_mode,
+ operation_mode)
+
+ def turn_away_mode_on(self):
+ """Turn away mode on."""
+ raise NotImplementedError()
+
+ async def async_turn_away_mode_on(self):
+ """Turn away mode on."""
+ await self.hass.async_add_executor_job(self.turn_away_mode_on)
+
+ def turn_away_mode_off(self):
+ """Turn away mode off."""
+ raise NotImplementedError()
+
+ async def async_turn_away_mode_off(self):
+ """Turn away mode off."""
+ await self.hass.async_add_executor_job(self.turn_away_mode_off)
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ raise NotImplementedError()
+
+ @property
+ def min_temp(self):
+ """Return the minimum temperature."""
+ return convert_temperature(DEFAULT_MIN_TEMP, TEMP_FAHRENHEIT,
+ self.temperature_unit)
+
+ @property
+ def max_temp(self):
+ """Return the maximum temperature."""
+ return convert_temperature(DEFAULT_MAX_TEMP, TEMP_FAHRENHEIT,
+ self.temperature_unit)
+
+
+async def async_service_away_mode(entity, service):
+ """Handle away mode service."""
+ if service.data[ATTR_AWAY_MODE]:
+ await entity.async_turn_away_mode_on()
+ else:
+ await entity.async_turn_away_mode_off()
+
+
+async def async_service_temperature_set(entity, service):
+ """Handle set temperature service."""
+ hass = entity.hass
+ kwargs = {}
+
+ for value, temp in service.data.items():
+ if value in CONVERTIBLE_ATTRIBUTE:
+ kwargs[value] = convert_temperature(
+ temp,
+ hass.config.units.temperature_unit,
+ entity.temperature_unit
+ )
+ else:
+ kwargs[value] = temp
+
+ await entity.async_set_temperature(**kwargs)
diff --git a/homeassistant/components/water_heater/demo.py b/homeassistant/components/water_heater/demo.py
new file mode 100644
index 00000000000..89b86c12af4
--- /dev/null
+++ b/homeassistant/components/water_heater/demo.py
@@ -0,0 +1,110 @@
+"""
+Demo platform that offers a fake water_heater device.
+
+For more details about this platform, please refer to the documentation
+https://home-assistant.io/components/demo/
+"""
+from homeassistant.components.water_heater import (
+ WaterHeaterDevice,
+ SUPPORT_TARGET_TEMPERATURE,
+ SUPPORT_AWAY_MODE,
+ SUPPORT_OPERATION_MODE)
+from homeassistant.const import TEMP_FAHRENHEIT, ATTR_TEMPERATURE, TEMP_CELSIUS
+
+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 Demo water_heater devices."""
+ add_entities([
+ DemoWaterHeater('Demo Water Heater', 119,
+ TEMP_FAHRENHEIT, False, 'eco'),
+ DemoWaterHeater('Demo Water Heater Celsius', 45,
+ TEMP_CELSIUS, True, 'eco')
+
+ ])
+
+
+class DemoWaterHeater(WaterHeaterDevice):
+ """Representation of a demo water_heater device."""
+
+ def __init__(self, name, target_temperature, unit_of_measurement,
+ away, current_operation):
+ """Initialize the water_heater device."""
+ self._name = name
+ self._support_flags = SUPPORT_FLAGS_HEATER
+ if target_temperature is not None:
+ self._support_flags = \
+ self._support_flags | SUPPORT_TARGET_TEMPERATURE
+ if away is not None:
+ self._support_flags = self._support_flags | SUPPORT_AWAY_MODE
+ if current_operation is not None:
+ self._support_flags = self._support_flags | SUPPORT_OPERATION_MODE
+ self._target_temperature = target_temperature
+ self._unit_of_measurement = unit_of_measurement
+ self._away = away
+ self._current_operation = current_operation
+ self._operation_list = ['eco', 'electric', 'performance',
+ 'high_demand', 'heat_pump', 'gas',
+ 'off']
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ return self._support_flags
+
+ @property
+ def should_poll(self):
+ """Return the polling state."""
+ return False
+
+ @property
+ def name(self):
+ """Return the name of the water_heater device."""
+ return self._name
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement."""
+ return self._unit_of_measurement
+
+ @property
+ def target_temperature(self):
+ """Return the temperature we try to reach."""
+ return self._target_temperature
+
+ @property
+ def current_operation(self):
+ """Return current operation ie. heat, cool, idle."""
+ return self._current_operation
+
+ @property
+ def operation_list(self):
+ """Return the list of available operation modes."""
+ return self._operation_list
+
+ @property
+ def is_away_mode_on(self):
+ """Return if away mode is on."""
+ return self._away
+
+ def set_temperature(self, **kwargs):
+ """Set new target temperatures."""
+ self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
+ self.schedule_update_ha_state()
+
+ def set_operation_mode(self, operation_mode):
+ """Set new operation mode."""
+ self._current_operation = operation_mode
+ self.schedule_update_ha_state()
+
+ def turn_away_mode_on(self):
+ """Turn away mode on."""
+ self._away = True
+ self.schedule_update_ha_state()
+
+ def turn_away_mode_off(self):
+ """Turn away mode off."""
+ self._away = False
+ self.schedule_update_ha_state()
diff --git a/homeassistant/components/climate/econet.py b/homeassistant/components/water_heater/econet.py
similarity index 96%
rename from homeassistant/components/climate/econet.py
rename to homeassistant/components/water_heater/econet.py
index 8be640c37e1..6af8ea43fa6 100644
--- a/homeassistant/components/climate/econet.py
+++ b/homeassistant/components/water_heater/econet.py
@@ -2,17 +2,17 @@
Support for Rheem EcoNet water heaters.
For more details about this platform, please refer to the documentation at
-https://home-assistant.io/components/climate.econet/
+https://home-assistant.io/components/water_heater.econet/
"""
import datetime
import logging
import voluptuous as vol
-from homeassistant.components.climate import (
+from homeassistant.components.water_heater import (
DOMAIN, PLATFORM_SCHEMA, STATE_ECO, STATE_ELECTRIC, STATE_GAS,
STATE_HEAT_PUMP, STATE_HIGH_DEMAND, STATE_OFF, STATE_PERFORMANCE,
- SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice)
+ SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, WaterHeaterDevice)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_PASSWORD, CONF_USERNAME,
TEMP_FAHRENHEIT)
@@ -109,7 +109,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
schema=DELETE_VACATION_SCHEMA)
-class EcoNetWaterHeater(ClimateDevice):
+class EcoNetWaterHeater(WaterHeaterDevice):
"""Representation of an EcoNet water heater."""
def __init__(self, water_heater):
diff --git a/homeassistant/components/water_heater/services.yaml b/homeassistant/components/water_heater/services.yaml
new file mode 100644
index 00000000000..72a3f909fbb
--- /dev/null
+++ b/homeassistant/components/water_heater/services.yaml
@@ -0,0 +1,51 @@
+# Describes the format for available water_heater services
+
+set_away_mode:
+ description: Turn away mode on/off for water_heater device.
+ fields:
+ entity_id:
+ description: Name(s) of entities to change.
+ example: 'water_heater.water_heater'
+ away_mode:
+ description: New value of away mode.
+ example: true
+
+set_temperature:
+ description: Set target temperature of water_heater device.
+ fields:
+ entity_id:
+ description: Name(s) of entities to change.
+ example: 'water_heater.water_heater'
+ temperature:
+ description: New target temperature for water heater.
+ example: 25
+
+set_operation_mode:
+ description: Set operation mode for water_heater device.
+ fields:
+ entity_id:
+ description: Name(s) of entities to change.
+ example: 'water_heater.water_heater'
+ operation_mode:
+ description: New value of operation mode.
+ example: eco
+
+econet_add_vacation:
+ description: Add a vacation to your water heater.
+ fields:
+ entity_id:
+ description: Name(s) of entities to change.
+ example: 'water_heater.econet'
+ 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: 'water_heater.econet'
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/wink.py b/homeassistant/components/water_heater/wink.py
new file mode 100644
index 00000000000..a840baf980a
--- /dev/null
+++ b/homeassistant/components/water_heater/wink.py
@@ -0,0 +1,136 @@
+"""
+Support for Wink water heaters.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/water_heater.wink/
+"""
+import logging
+
+from homeassistant.components.water_heater import (
+ ATTR_TEMPERATURE, STATE_ECO, STATE_ELECTRIC,
+ STATE_PERFORMANCE, SUPPORT_AWAY_MODE, STATE_HEAT_PUMP,
+ STATE_GAS, STATE_HIGH_DEMAND,
+ SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
+ WaterHeaterDevice)
+from homeassistant.components.wink import DOMAIN, WinkDevice
+from homeassistant.const import (
+ STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS)
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
+ SUPPORT_AWAY_MODE)
+
+ATTR_RHEEM_TYPE = 'rheem_type'
+ATTR_VACATION_MODE = 'vacation_mode'
+
+HA_STATE_TO_WINK = {
+ STATE_ECO: 'eco',
+ STATE_ELECTRIC: 'electric_only',
+ STATE_GAS: 'gas',
+ 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()}
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the Wink water heater devices."""
+ import pywink
+ 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 WinkWaterHeater(WinkDevice, WaterHeaterDevice):
+ """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()
diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py
index f9a8f1fbbe4..725c7f609a7 100644
--- a/homeassistant/components/weather/__init__.py
+++ b/homeassistant/components/weather/__init__.py
@@ -40,12 +40,21 @@ ATTR_WEATHER_WIND_SPEED = 'wind_speed'
async def async_setup(hass, config):
"""Set up the weather component."""
- component = EntityComponent(_LOGGER, DOMAIN, hass)
-
+ component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass)
await component.async_setup(config)
return True
+async def async_setup_entry(hass, entry):
+ """Set up a config entry."""
+ return await hass.data[DOMAIN].async_setup_entry(entry)
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a config entry."""
+ return await hass.data[DOMAIN].async_unload_entry(entry)
+
+
class WeatherEntity(Entity):
"""ABC for weather data."""
diff --git a/homeassistant/components/weather/met.py b/homeassistant/components/weather/met.py
index f888af2e909..bab6624e9d0 100644
--- a/homeassistant/components/weather/met.py
+++ b/homeassistant/components/weather/met.py
@@ -16,9 +16,9 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import (async_track_utc_time_change,
async_call_later)
-from homeassistant.util import dt as dt_util
+import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['pyMetno==0.2.0']
+REQUIREMENTS = ['pyMetno==0.3.0']
_LOGGER = logging.getLogger(__name__)
@@ -26,49 +26,6 @@ CONF_ATTRIBUTION = "Weather forecast from met.no, delivered " \
"by the Norwegian Meteorological Institute."
DEFAULT_NAME = "Met.no"
-# https://api.met.no/weatherapi/weathericon/_/documentation/#___top
-CONDITIONS = {1: 'sunny',
- 2: 'partlycloudy',
- 3: 'partlycloudy',
- 4: 'cloudy',
- 5: 'rainy',
- 6: 'lightning-rainy',
- 7: 'snowy-rainy',
- 8: 'snowy',
- 9: 'rainy',
- 10: 'rainy',
- 11: 'lightning-rainy',
- 12: 'snowy-rainy',
- 13: 'snowy',
- 14: 'snowy',
- 15: 'fog',
- 20: 'lightning-rainy',
- 21: 'lightning-rainy',
- 22: 'lightning-rainy',
- 23: 'lightning-rainy',
- 24: 'lightning-rainy',
- 25: 'lightning-rainy',
- 26: 'lightning-rainy',
- 27: 'lightning-rainy',
- 28: 'lightning-rainy',
- 29: 'lightning-rainy',
- 30: 'lightning-rainy',
- 31: 'lightning-rainy',
- 32: 'lightning-rainy',
- 33: 'lightning-rainy',
- 34: 'lightning-rainy',
- 40: 'rainy',
- 41: 'rainy',
- 42: 'snowy-rainy',
- 43: 'snowy-rainy',
- 44: 'snowy',
- 45: 'snowy',
- 46: 'rainy',
- 47: 'snowy-rainy',
- 48: 'snowy-rainy',
- 49: 'snowy',
- 50: 'snowy',
- }
URL = 'https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -113,12 +70,8 @@ class MetWeather(WeatherEntity):
clientsession,
URL
)
- self._temperature = None
- self._condition = None
- self._pressure = None
- self._humidity = None
- self._wind_speed = None
- self._wind_bearing = None
+ self._current_weather_data = {}
+ self._forecast_data = None
async def async_added_to_hass(self):
"""Start fetching data."""
@@ -145,38 +98,9 @@ class MetWeather(WeatherEntity):
async def _update(self, *_):
"""Get the latest data from Met.no."""
- import metno
- if self._weather_data is None:
- return
-
- now = dt_util.utcnow()
-
- ordered_entries = []
- for time_entry in self._weather_data.data['product']['time']:
- valid_from = dt_util.parse_datetime(time_entry['@from'])
- valid_to = dt_util.parse_datetime(time_entry['@to'])
-
- if now >= valid_to:
- # Has already passed. Never select this.
- continue
-
- average_dist = (abs((valid_to - now).total_seconds()) +
- abs((valid_from - now).total_seconds()))
-
- ordered_entries.append((average_dist, time_entry))
-
- if not ordered_entries:
- return
- ordered_entries.sort(key=lambda item: item[0])
-
- self._temperature = metno.get_forecast('temperature', ordered_entries)
- self._condition = CONDITIONS.get(metno.get_forecast('symbol',
- ordered_entries))
- self._pressure = metno.get_forecast('pressure', ordered_entries)
- self._humidity = metno.get_forecast('humidity', ordered_entries)
- self._wind_speed = metno.get_forecast('windSpeed', ordered_entries)
- self._wind_bearing = metno.get_forecast('windDirection',
- ordered_entries)
+ self._current_weather_data = self._weather_data.get_current_weather()
+ time_zone = dt_util.DEFAULT_TIME_ZONE
+ self._forecast_data = self._weather_data.get_forecast(time_zone)
self.async_schedule_update_ha_state()
@property
@@ -187,12 +111,12 @@ class MetWeather(WeatherEntity):
@property
def condition(self):
"""Return the current condition."""
- return self._condition
+ return self._current_weather_data.get('condition')
@property
def temperature(self):
"""Return the temperature."""
- return self._temperature
+ return self._current_weather_data.get('temperature')
@property
def temperature_unit(self):
@@ -202,24 +126,29 @@ class MetWeather(WeatherEntity):
@property
def pressure(self):
"""Return the pressure."""
- return self._pressure
+ return self._current_weather_data.get('pressure')
@property
def humidity(self):
"""Return the humidity."""
- return self._humidity
+ return self._current_weather_data.get('humidity')
@property
def wind_speed(self):
"""Return the wind speed."""
- return self._wind_speed
+ return self._current_weather_data.get('wind_speed')
@property
def wind_bearing(self):
"""Return the wind direction."""
- return self._wind_bearing
+ return self._current_weather_data.get('wind_bearing')
@property
def attribution(self):
"""Return the attribution."""
return CONF_ATTRIBUTION
+
+ @property
+ def forecast(self):
+ """Return the forecast array."""
+ return self._forecast_data
diff --git a/homeassistant/components/weather/smhi.py b/homeassistant/components/weather/smhi.py
new file mode 100644
index 00000000000..3bbaab3f8ec
--- /dev/null
+++ b/homeassistant/components/weather/smhi.py
@@ -0,0 +1,242 @@
+"""Support for the Swedish weather institute weather service.
+
+For more details about this platform, please refer to the documentation
+https://home-assistant.io/components/weather.smhi/
+"""
+
+
+import asyncio
+import logging
+from datetime import timedelta
+from typing import Dict, List
+
+import aiohttp
+import async_timeout
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ CONF_LATITUDE, CONF_LONGITUDE,
+ CONF_NAME, TEMP_CELSIUS)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import aiohttp_client
+from homeassistant.util import dt, Throttle
+
+from homeassistant.components.weather import (
+ WeatherEntity, ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP,
+ ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME,
+ ATTR_FORECAST_PRECIPITATION)
+
+from homeassistant.components.smhi.const import (
+ ENTITY_ID_SENSOR_FORMAT, ATTR_SMHI_CLOUDINESS)
+
+DEPENDENCIES = ['smhi']
+
+_LOGGER = logging.getLogger(__name__)
+
+# Used to map condition from API results
+CONDITION_CLASSES = {
+ 'cloudy': [5, 6],
+ 'fog': [7],
+ 'hail': [],
+ 'lightning': [21],
+ 'lightning-rainy': [11],
+ 'partlycloudy': [3, 4],
+ 'pouring': [10, 20],
+ 'rainy': [8, 9, 18, 19],
+ 'snowy': [15, 16, 17, 25, 26, 27],
+ 'snowy-rainy': [12, 13, 14, 22, 23, 24],
+ 'sunny': [1, 2],
+ 'windy': [],
+ 'windy-variant': [],
+ 'exceptional': [],
+}
+
+
+# 5 minutes between retrying connect to API again
+RETRY_TIMEOUT = 5*60
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=31)
+
+
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Old way of setting up components.
+
+ Can only be called when a user accidentally mentions smhi in the
+ config. In that case it will be ignored.
+ """
+ pass
+
+
+async def async_setup_entry(hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ config_entries) -> bool:
+ """Add a weather entity from map location."""
+ location = config_entry.data
+ name = location[CONF_NAME]
+
+ session = aiohttp_client.async_get_clientsession(hass)
+
+ entity = SmhiWeather(name, location[CONF_LATITUDE],
+ location[CONF_LONGITUDE],
+ session=session)
+ entity.entity_id = ENTITY_ID_SENSOR_FORMAT.format(name)
+
+ config_entries([entity], True)
+ return True
+
+
+class SmhiWeather(WeatherEntity):
+ """Representation of a weather entity."""
+
+ def __init__(self, name: str, latitude: str,
+ longitude: str,
+ session: aiohttp.ClientSession = None) -> None:
+ """Initialize the SMHI weather entity."""
+ from smhi import Smhi
+
+ self._name = name
+ self._latitude = latitude
+ self._longitude = longitude
+ self._forecasts = None
+ self._fail_count = 0
+ self._smhi_api = Smhi(self._longitude, self._latitude,
+ session=session)
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ async def async_update(self) -> None:
+ """Refresh the forecast data from SMHI weather API."""
+ from smhi.smhi_lib import SmhiForecastException
+
+ def fail():
+ self._fail_count += 1
+ if self._fail_count < 3:
+ self.hass.helpers.event.async_call_later(
+ RETRY_TIMEOUT, self.retry_update())
+
+ try:
+ with async_timeout.timeout(10, loop=self.hass.loop):
+ self._forecasts = await self.get_weather_forecast()
+ self._fail_count = 0
+
+ except (asyncio.TimeoutError, SmhiForecastException):
+ _LOGGER.error("Failed to connect to SMHI API, "
+ "retry in 5 minutes")
+ fail()
+
+ async def retry_update(self):
+ """Retry refresh weather forecast."""
+ self.async_update()
+
+ async def get_weather_forecast(self) -> []:
+ """Return the current forecasts from SMHI API."""
+ return await self._smhi_api.async_get_forecast()
+
+ @property
+ def name(self) -> str:
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def temperature(self) -> int:
+ """Return the temperature."""
+ if self._forecasts is not None:
+ return self._forecasts[0].temperature
+ return None
+
+ @property
+ def temperature_unit(self) -> str:
+ """Return the unit of measurement."""
+ return TEMP_CELSIUS
+
+ @property
+ def humidity(self) -> int:
+ """Return the humidity."""
+ if self._forecasts is not None:
+ return self._forecasts[0].humidity
+ return None
+
+ @property
+ def wind_speed(self) -> float:
+ """Return the wind speed."""
+ if self._forecasts is not None:
+ # Convert from m/s to km/h
+ return round(self._forecasts[0].wind_speed*18/5)
+ return None
+
+ @property
+ def wind_bearing(self) -> int:
+ """Return the wind bearing."""
+ if self._forecasts is not None:
+ return self._forecasts[0].wind_direction
+ return None
+
+ @property
+ def visibility(self) -> float:
+ """Return the visibility."""
+ if self._forecasts is not None:
+ return self._forecasts[0].horizontal_visibility
+ return None
+
+ @property
+ def pressure(self) -> int:
+ """Return the pressure."""
+ if self._forecasts is not None:
+ return self._forecasts[0].pressure
+ return None
+
+ @property
+ def cloudiness(self) -> int:
+ """Return the cloudiness."""
+ if self._forecasts is not None:
+ return self._forecasts[0].cloudiness
+ return None
+
+ @property
+ def condition(self) -> str:
+ """Return the weather condition."""
+ if self._forecasts is None:
+ return None
+ return next((
+ k for k, v in CONDITION_CLASSES.items()
+ if self._forecasts[0].symbol in v), None)
+
+ @property
+ def attribution(self) -> str:
+ """Return the attribution."""
+ return 'Swedish weather institute (SMHI)'
+
+ @property
+ def forecast(self) -> List:
+ """Return the forecast."""
+ if self._forecasts is None:
+ return None
+
+ data = []
+ for forecast in self._forecasts:
+ condition = next((
+ k for k, v in CONDITION_CLASSES.items()
+ if forecast.symbol in v), None)
+
+ # Only get mid day forecasts
+ if forecast.valid_time.hour == 12:
+ data.append({
+ ATTR_FORECAST_TIME:
+ dt.as_local(forecast.valid_time),
+ ATTR_FORECAST_TEMP:
+ forecast.temperature_max,
+ ATTR_FORECAST_TEMP_LOW:
+ forecast.temperature_min,
+ ATTR_FORECAST_PRECIPITATION:
+ round(forecast.mean_precipitation*24),
+ ATTR_FORECAST_CONDITION:
+ condition
+ })
+
+ return data
+
+ @property
+ def device_state_attributes(self) -> Dict:
+ """Return SMHI specific attributes."""
+ if self.cloudiness:
+ return {ATTR_SMHI_CLOUDINESS: self.cloudiness}
diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py
index 505c287a99e..567b8e235a8 100644
--- a/homeassistant/components/weather/yweather.py
+++ b/homeassistant/components/weather/yweather.py
@@ -31,21 +31,21 @@ DEFAULT_NAME = 'Yweather'
SCAN_INTERVAL = timedelta(minutes=10)
CONDITION_CLASSES = {
- 'clear-night': [31],
- 'cloudy': [26, 27, 28, 29, 30],
- 'fog': [19, 20, 21, 22, 23],
- 'hail': [17, 18, 35],
- 'lightning': [37],
- 'lightning-rainy': [3, 4, 38, 39, 47],
- 'partlycloudy': [44],
- 'pouring': [40, 45],
- 'rainy': [9, 11, 12],
- 'snowy': [8, 13, 14, 15, 16, 41, 42, 43],
- 'snowy-rainy': [5, 6, 7, 10, 46],
- 'sunny': [32, 33, 34, 25, 36],
- 'windy': [24],
+ 'clear-night': [31, 33],
+ 'cloudy': [26, 27, 28],
+ 'fog': [20, 21],
+ 'hail': [17, 35],
+ 'lightning': [],
+ 'lightning-rainy': [3, 4, 37, 38, 39, 45, 47],
+ 'partlycloudy': [29, 30, 44],
+ 'pouring': [],
+ 'rainy': [9, 10, 11, 12, 40],
+ 'snowy': [8, 13, 14, 15, 16, 41, 42, 43, 46],
+ 'snowy-rainy': [5, 6, 7, 18],
+ 'sunny': [25, 32, 34, 36],
+ 'windy': [23, 24],
'windy-variant': [],
- 'exceptional': [0, 1, 2],
+ 'exceptional': [0, 1, 2, 19, 22],
}
diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py
index d21ccc18c93..3db044c4d1b 100644
--- a/homeassistant/components/wink/__init__.py
+++ b/homeassistant/components/wink/__init__.py
@@ -177,7 +177,7 @@ DIAL_STATE_SCHEMA = vol.Schema({
WINK_COMPONENTS = [
'binary_sensor', 'sensor', 'light', 'switch', 'lock', 'cover', 'climate',
- 'fan', 'alarm_control_panel', 'scene'
+ 'fan', 'alarm_control_panel', 'scene', 'water_heater'
]
WINK_HUBS = []
diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py
index 27414a64150..aa2102ca805 100644
--- a/homeassistant/components/xiaomi_aqara.py
+++ b/homeassistant/components/xiaomi_aqara.py
@@ -22,7 +22,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
from homeassistant.util import slugify
-REQUIREMENTS = ['PyXiaomiGateway==0.11.0']
+REQUIREMENTS = ['PyXiaomiGateway==0.11.1']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py
index 2e74e079d5f..8cea746f89a 100644
--- a/homeassistant/components/zha/__init__.py
+++ b/homeassistant/components/zha/__init__.py
@@ -276,15 +276,23 @@ class ApplicationListener:
device_classes, discovery_attr,
is_new_join):
"""Try to set up an entity from a "bare" cluster."""
+ import homeassistant.components.zha.const as zha_const
if cluster.cluster_id in profile_clusters:
return
- component = None
+ component = sub_component = None
for cluster_type, candidate_component in device_classes.items():
if isinstance(cluster, cluster_type):
component = candidate_component
break
+ for signature, comp in zha_const.CUSTOM_CLUSTER_MAPPINGS.items():
+ if (isinstance(endpoint.device, signature[0]) and
+ cluster.cluster_id == signature[1]):
+ component = comp[0]
+ sub_component = comp[1]
+ break
+
if component is None:
return
@@ -301,6 +309,8 @@ class ApplicationListener:
'entity_suffix': '_{}'.format(cluster.cluster_id),
}
discovery_info[discovery_attr] = {cluster.cluster_id: cluster}
+ if sub_component:
+ discovery_info.update({'sub_component': sub_component})
self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info
await discovery.async_load_platform(
diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py
index 37c7f5592a0..0b3e926fadc 100644
--- a/homeassistant/components/zha/const.py
+++ b/homeassistant/components/zha/const.py
@@ -3,6 +3,7 @@
DEVICE_CLASS = {}
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
+CUSTOM_CLUSTER_MAPPINGS = {}
COMPONENT_CLUSTERS = {}
@@ -12,8 +13,9 @@ def populate_data():
These cannot be module level, as importing bellows must be done in a
in a function.
"""
- from zigpy import zcl
+ from zigpy import zcl, quirks
from zigpy.profiles import PROFILES, zha, zll
+ from homeassistant.components.sensor import zha as sensor_zha
DEVICE_CLASS[zha.PROFILE_ID] = {
zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
@@ -58,6 +60,12 @@ def populate_data():
zcl.clusters.general.OnOff: 'binary_sensor',
})
+ # A map of device/cluster to component/sub-component
+ CUSTOM_CLUSTER_MAPPINGS.update({
+ (quirks.smartthings.SmartthingsTemperatureHumiditySensor, 64581):
+ ('sensor', sensor_zha.RelativeHumiditySensor)
+ })
+
# A map of hass components to all Zigbee clusters it could use
for profile_id, classes in DEVICE_CLASS.items():
profile = PROFILES[profile_id]
diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder/__init__.py
similarity index 78%
rename from homeassistant/components/zoneminder.py
rename to homeassistant/components/zoneminder/__init__.py
index 3f6d8ba7fcf..e5d0c7a5a92 100644
--- a/homeassistant/components/zoneminder.py
+++ b/homeassistant/components/zoneminder/__init__.py
@@ -10,12 +10,12 @@ import voluptuous as vol
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_PATH, CONF_SSL, CONF_USERNAME,
- CONF_VERIFY_SSL)
+ CONF_VERIFY_SSL, ATTR_NAME)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['zm-py==0.0.5']
+REQUIREMENTS = ['zm-py==0.1.0']
CONF_PATH_ZMS = 'path_zms'
@@ -38,6 +38,11 @@ CONFIG_SCHEMA = vol.Schema({
})
}, extra=vol.ALLOW_EXTRA)
+SERVICE_SET_RUN_STATE = 'set_run_state'
+SET_RUN_STATE_SCHEMA = vol.Schema({
+ vol.Required(ATTR_NAME): cv.string
+})
+
def setup(hass, config):
"""Set up the ZoneMinder component."""
@@ -57,4 +62,13 @@ def setup(hass, config):
conf.get(CONF_PATH_ZMS),
conf.get(CONF_VERIFY_SSL))
+ def set_active_state(call):
+ """Set the ZoneMinder run state to the given state name."""
+ return hass.data[DOMAIN].set_active_state(call.data[ATTR_NAME])
+
+ hass.services.register(
+ DOMAIN, SERVICE_SET_RUN_STATE, set_active_state,
+ schema=SET_RUN_STATE_SCHEMA
+ )
+
return hass.data[DOMAIN].login()
diff --git a/homeassistant/components/zoneminder/services.yaml b/homeassistant/components/zoneminder/services.yaml
new file mode 100644
index 00000000000..e6346d2f384
--- /dev/null
+++ b/homeassistant/components/zoneminder/services.yaml
@@ -0,0 +1,6 @@
+set_run_state:
+ description: Set the ZoneMinder run state
+ fields:
+ name:
+ description: The string name of the ZoneMinder run state to set as active.
+ example: 'Home'
\ No newline at end of file
diff --git a/homeassistant/components/zwave/.translations/es.json b/homeassistant/components/zwave/.translations/es.json
new file mode 100644
index 00000000000..8c287d9a539
--- /dev/null
+++ b/homeassistant/components/zwave/.translations/es.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "one_instance_only": "El componente solo admite una instancia de Z-Wave"
+ },
+ "error": {
+ "option_error": "Z-Wave error de validaci\u00f3n. \u00bfLa ruta de acceso a la memoria USB escorrecta?"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "network_key": "Clave de red (d\u00e9jelo en blanco para generar autom\u00e1ticamente)",
+ "usb_path": "Ruta USB"
+ },
+ "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n",
+ "title": "Configurar Z-Wave"
+ }
+ },
+ "title": "Z-Wave"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave/.translations/fr.json b/homeassistant/components/zwave/.translations/fr.json
new file mode 100644
index 00000000000..c667965bebc
--- /dev/null
+++ b/homeassistant/components/zwave/.translations/fr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Z-Wave est d\u00e9j\u00e0 configur\u00e9",
+ "one_instance_only": "Le composant ne prend en charge qu'une seule instance Z-Wave"
+ },
+ "error": {
+ "option_error": "La validation Z-Wave a \u00e9chou\u00e9. Le chemin d'acc\u00e8s \u00e0 la cl\u00e9 USB est-il correct?"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "network_key": "Cl\u00e9 r\u00e9seau (laisser vide pour g\u00e9n\u00e9rer automatiquement)",
+ "usb_path": "Chemin USB"
+ },
+ "title": "Configurer Z-Wave"
+ }
+ },
+ "title": "Z-Wave"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave/.translations/hu.json b/homeassistant/components/zwave/.translations/hu.json
index 16c25cb7cab..e2acc5f9115 100644
--- a/homeassistant/components/zwave/.translations/hu.json
+++ b/homeassistant/components/zwave/.translations/hu.json
@@ -1,5 +1,16 @@
{
"config": {
+ "abort": {
+ "already_configured": "A Z-Wave m\u00e1r konfigur\u00e1lva van"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "network_key": "H\u00e1l\u00f3zati kulcs (hagyja \u00fcresen az automatikus gener\u00e1l\u00e1shoz)",
+ "usb_path": "USB el\u00e9r\u00e9si \u00fat"
+ }
+ }
+ },
"title": "Z-Wave"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/zwave/.translations/ko.json b/homeassistant/components/zwave/.translations/ko.json
index d57f758ce25..e288019de0c 100644
--- a/homeassistant/components/zwave/.translations/ko.json
+++ b/homeassistant/components/zwave/.translations/ko.json
@@ -13,7 +13,7 @@
"network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4 (\uacf5\ub780\uc73c\ub85c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4)",
"usb_path": "USB \uacbd\ub85c"
},
- "description": "\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/docs/z-wave/installation/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694",
+ "description": "\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/docs/z-wave/installation/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694",
"title": "Z-Wave \uc124\uc815"
}
},
diff --git a/homeassistant/components/zwave/.translations/pt.json b/homeassistant/components/zwave/.translations/pt.json
new file mode 100644
index 00000000000..6962f077498
--- /dev/null
+++ b/homeassistant/components/zwave/.translations/pt.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "O Z-Wave j\u00e1 est\u00e1 configurado",
+ "one_instance_only": "Componente suporta apenas uma inst\u00e2ncia Z-Wave"
+ },
+ "error": {
+ "option_error": "A valida\u00e7\u00e3o Z-Wave falhou. O caminho para o stick USB est\u00e1 correto?"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "network_key": "Network Key (deixe em branco para auto-gera\u00e7\u00e3o)",
+ "usb_path": "Endere\u00e7o USB"
+ },
+ "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o",
+ "title": "Configurar o Z-Wave"
+ }
+ },
+ "title": "Z-Wave"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave/.translations/tr.json b/homeassistant/components/zwave/.translations/tr.json
new file mode 100644
index 00000000000..c9762784d52
--- /dev/null
+++ b/homeassistant/components/zwave/.translations/tr.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "network_key": "A\u011f Anajtar\u0131 (otomatik \u00fcretilmesi i\u00e7in bo\u015f b\u0131rak\u0131n\u0131z)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py
index fa78f719557..35703d64974 100644
--- a/homeassistant/components/zwave/__init__.py
+++ b/homeassistant/components/zwave/__init__.py
@@ -11,6 +11,7 @@ from pprint import pprint
import voluptuous as vol
+from homeassistant import config_entries
from homeassistant.core import callback, CoreState
from homeassistant.loader import get_platform
from homeassistant.helpers import discovery
@@ -28,24 +29,28 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
from . import const
-from .const import DOMAIN, DATA_DEVICES, DATA_NETWORK, DATA_ENTITY_VALUES
+from . import config_flow # noqa # pylint: disable=unused-import
+from .const import (
+ CONF_AUTOHEAL, CONF_DEBUG, CONF_POLLING_INTERVAL,
+ CONF_USB_STICK_PATH, CONF_CONFIG_PATH, CONF_NETWORK_KEY,
+ DEFAULT_CONF_AUTOHEAL, DEFAULT_CONF_USB_STICK_PATH,
+ DEFAULT_POLLING_INTERVAL, DEFAULT_DEBUG, DOMAIN,
+ DATA_DEVICES, DATA_NETWORK, DATA_ENTITY_VALUES)
from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity
from . import workaround
from .discovery_schemas import DISCOVERY_SCHEMAS
from .util import (check_node_schema, check_value_schema, node_name,
check_has_unique_id, is_node_parsed)
-REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.9']
+REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.10']
_LOGGER = logging.getLogger(__name__)
CLASS_ID = 'class_id'
-CONF_AUTOHEAL = 'autoheal'
-CONF_DEBUG = 'debug'
+
+ATTR_POWER = 'power_consumption'
+
CONF_POLLING_INTENSITY = 'polling_intensity'
-CONF_POLLING_INTERVAL = 'polling_interval'
-CONF_USB_STICK_PATH = 'usb_path'
-CONF_CONFIG_PATH = 'config_path'
CONF_IGNORED = 'ignored'
CONF_INVERT_OPENCLOSE_BUTTONS = 'invert_openclose_buttons'
CONF_REFRESH_VALUE = 'refresh_value'
@@ -53,19 +58,17 @@ CONF_REFRESH_DELAY = 'delay'
CONF_DEVICE_CONFIG = 'device_config'
CONF_DEVICE_CONFIG_GLOB = 'device_config_glob'
CONF_DEVICE_CONFIG_DOMAIN = 'device_config_domain'
-CONF_NETWORK_KEY = 'network_key'
-ATTR_POWER = 'power_consumption'
+DATA_ZWAVE_CONFIG = 'zwave_config'
-DEFAULT_CONF_AUTOHEAL = True
-DEFAULT_CONF_USB_STICK_PATH = '/zwaveusbstick'
-DEFAULT_POLLING_INTERVAL = 60000
-DEFAULT_DEBUG = False
DEFAULT_CONF_IGNORED = False
DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False
DEFAULT_CONF_REFRESH_VALUE = False
DEFAULT_CONF_REFRESH_DELAY = 5
+SUPPORTED_PLATFORMS = ['binary_sensor', 'climate', 'cover', 'fan',
+ 'light', 'sensor', 'switch']
+
RENAME_NODE_SCHEMA = vol.Schema({
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
vol.Required(const.ATTR_NAME): cv.string,
@@ -224,13 +227,32 @@ async def async_setup_platform(hass, config, async_add_entities,
discovery_info[const.DISCOVERY_DEVICE], None)
if device is None:
return False
-
async_add_entities([device])
return True
async def async_setup(hass, config):
- """Set up Z-Wave.
+ """Set up Z-Wave components."""
+ if DOMAIN not in config:
+ return True
+
+ conf = config[DOMAIN]
+ hass.data[DATA_ZWAVE_CONFIG] = conf
+
+ if not hass.config_entries.async_entries(DOMAIN):
+ hass.async_create_task(hass.config_entries.flow.async_init(
+ DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
+ data={
+ CONF_USB_STICK_PATH: conf[CONF_USB_STICK_PATH],
+ CONF_NETWORK_KEY: conf.get(CONF_NETWORK_KEY),
+ }
+ ))
+
+ return True
+
+
+async def async_setup_entry(hass, config_entry):
+ """Set up Z-Wave from a config entry.
Will automatically load components to support devices found on the network.
"""
@@ -240,27 +262,31 @@ async def async_setup(hass, config):
from openzwave.network import ZWaveNetwork
from openzwave.group import ZWaveGroup
+ config = {}
+ if DATA_ZWAVE_CONFIG in hass.data:
+ config = hass.data[DATA_ZWAVE_CONFIG]
+
# Load configuration
- use_debug = config[DOMAIN].get(CONF_DEBUG)
- autoheal = config[DOMAIN].get(CONF_AUTOHEAL)
+ use_debug = config.get(CONF_DEBUG, DEFAULT_DEBUG)
+ autoheal = config.get(CONF_AUTOHEAL,
+ DEFAULT_CONF_AUTOHEAL)
device_config = EntityValues(
- config[DOMAIN][CONF_DEVICE_CONFIG],
- config[DOMAIN][CONF_DEVICE_CONFIG_DOMAIN],
- config[DOMAIN][CONF_DEVICE_CONFIG_GLOB])
+ config.get(CONF_DEVICE_CONFIG),
+ config.get(CONF_DEVICE_CONFIG_DOMAIN),
+ config.get(CONF_DEVICE_CONFIG_GLOB))
# Setup options
options = ZWaveOption(
- config[DOMAIN].get(CONF_USB_STICK_PATH),
+ config_entry.data[CONF_USB_STICK_PATH],
user_path=hass.config.config_dir,
- config_path=config[DOMAIN].get(CONF_CONFIG_PATH))
+ config_path=config.get(CONF_CONFIG_PATH))
options.set_console_output(use_debug)
- if CONF_NETWORK_KEY in config[DOMAIN]:
- options.addOption("NetworkKey", config[DOMAIN][CONF_NETWORK_KEY])
-
- options.lock()
+ if CONF_NETWORK_KEY in config_entry.data:
+ options.addOption("NetworkKey", config_entry.data[CONF_NETWORK_KEY])
+ await hass.async_add_executor_job(options.lock)
network = hass.data[DATA_NETWORK] = ZWaveNetwork(options, autostart=False)
hass.data[DATA_DEVICES] = {}
hass.data[DATA_ENTITY_VALUES] = []
@@ -666,7 +692,7 @@ async def async_setup(hass, config):
def _finalize_start():
"""Perform final initializations after Z-Wave network is awaked."""
polling_interval = convert(
- config[DOMAIN].get(CONF_POLLING_INTERVAL), int)
+ config.get(CONF_POLLING_INTERVAL), int)
if polling_interval is not None:
network.set_poll_interval(polling_interval, False)
@@ -691,8 +717,6 @@ async def async_setup(hass, config):
test_network)
hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK,
stop_network)
- hass.services.register(DOMAIN, const.SERVICE_START_NETWORK,
- start_zwave)
hass.services.register(DOMAIN, const.SERVICE_RENAME_NODE, rename_node,
schema=RENAME_NODE_SCHEMA)
hass.services.register(DOMAIN, const.SERVICE_RENAME_VALUE,
@@ -752,6 +776,13 @@ async def async_setup(hass, config):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_zwave)
+ hass.services.async_register(DOMAIN, const.SERVICE_START_NETWORK,
+ start_zwave)
+
+ for entry_component in SUPPORTED_PLATFORMS:
+ hass.async_create_task(hass.config_entries.async_forward_entry_setup(
+ config_entry, entry_component))
+
return True
@@ -903,9 +934,13 @@ class ZWaveDeviceEntityValues():
async def discover_device(component, device, dict_id):
"""Put device in a dictionary and call discovery on it."""
self._hass.data[DATA_DEVICES][dict_id] = device
- await discovery.async_load_platform(
- self._hass, component, DOMAIN,
- {const.DISCOVERY_DEVICE: dict_id}, self._zwave_config)
+ if component in SUPPORTED_PLATFORMS:
+ async_dispatcher_send(
+ self._hass, 'zwave_new_{}'.format(component), device)
+ else:
+ await discovery.async_load_platform(
+ self._hass, component, DOMAIN,
+ {const.DISCOVERY_DEVICE: dict_id}, self._zwave_config)
if device.unique_id:
self._hass.add_job(discover_device, component, device, dict_id)
@@ -985,6 +1020,18 @@ class ZWaveDeviceEntity(ZWaveBaseEntity):
"""Return a unique ID."""
return self._unique_id
+ @property
+ def device_info(self):
+ """Return device information."""
+ return {
+ 'identifiers': {
+ (DOMAIN, self.node_id)
+ },
+ 'manufacturer': self.node.manufacturer_name,
+ 'model': self.node.product_name,
+ 'name': node_name(self.node),
+ }
+
@property
def name(self):
"""Return the name of the device."""
diff --git a/homeassistant/components/zwave/config_flow.py b/homeassistant/components/zwave/config_flow.py
new file mode 100644
index 00000000000..2b853ffa81d
--- /dev/null
+++ b/homeassistant/components/zwave/config_flow.py
@@ -0,0 +1,95 @@
+"""Config flow to configure Z-Wave."""
+from collections import OrderedDict
+import logging
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+
+from .const import (
+ CONF_USB_STICK_PATH, CONF_NETWORK_KEY,
+ DEFAULT_CONF_USB_STICK_PATH, DOMAIN)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@config_entries.HANDLERS.register(DOMAIN)
+class ZwaveFlowHandler(config_entries.ConfigFlow):
+ """Handle a Z-Wave config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
+
+ def __init__(self):
+ """Initialize the Z-Wave config flow."""
+ self.usb_path = CONF_USB_STICK_PATH
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow start."""
+ if self._async_current_entries():
+ return self.async_abort(reason='one_instance_only')
+
+ errors = {}
+
+ fields = OrderedDict()
+ fields[vol.Required(CONF_USB_STICK_PATH,
+ default=DEFAULT_CONF_USB_STICK_PATH)] = str
+ fields[vol.Optional(CONF_NETWORK_KEY)] = str
+
+ if user_input is not None:
+ # Check if USB path is valid
+ from openzwave.option import ZWaveOption
+ from openzwave.object import ZWaveException
+
+ try:
+ from functools import partial
+ # pylint: disable=unused-variable
+ option = await self.hass.async_add_executor_job( # noqa: F841
+ partial(ZWaveOption,
+ user_input[CONF_USB_STICK_PATH],
+ user_path=self.hass.config.config_dir)
+ )
+ except ZWaveException:
+ errors['base'] = 'option_error'
+ return self.async_show_form(
+ step_id='user',
+ data_schema=vol.Schema(fields),
+ errors=errors
+ )
+
+ if user_input.get(CONF_NETWORK_KEY) is None:
+ # Generate a random key
+ from random import choice
+ key = str()
+ for i in range(16):
+ key += '0x'
+ key += choice('1234567890ABCDEF')
+ key += choice('1234567890ABCDEF')
+ if i < 15:
+ key += ', '
+ user_input[CONF_NETWORK_KEY] = key
+
+ return self.async_create_entry(
+ title='Z-Wave',
+ data={
+ CONF_USB_STICK_PATH: user_input[CONF_USB_STICK_PATH],
+ CONF_NETWORK_KEY: user_input[CONF_NETWORK_KEY],
+ },
+ )
+
+ return self.async_show_form(
+ step_id='user', data_schema=vol.Schema(fields)
+ )
+
+ async def async_step_import(self, info):
+ """Import existing configuration from Z-Wave."""
+ if self._async_current_entries():
+ return self.async_abort(reason='already_setup')
+
+ return self.async_create_entry(
+ title="Z-Wave (import from configuration.yaml)",
+ data={
+ CONF_USB_STICK_PATH: info.get(CONF_USB_STICK_PATH),
+ CONF_NETWORK_KEY: info.get(CONF_NETWORK_KEY),
+ },
+ )
diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py
index b84f0287349..fece48655df 100644
--- a/homeassistant/components/zwave/const.py
+++ b/homeassistant/components/zwave/const.py
@@ -22,6 +22,18 @@ ATTR_VALUE_INSTANCE = "value_instance"
NETWORK_READY_WAIT_SECS = 300
NODE_READY_WAIT_SECS = 30
+CONF_AUTOHEAL = 'autoheal'
+CONF_DEBUG = 'debug'
+CONF_POLLING_INTERVAL = 'polling_interval'
+CONF_USB_STICK_PATH = 'usb_path'
+CONF_CONFIG_PATH = 'config_path'
+CONF_NETWORK_KEY = 'network_key'
+
+DEFAULT_CONF_AUTOHEAL = True
+DEFAULT_CONF_USB_STICK_PATH = '/zwaveusbstick'
+DEFAULT_POLLING_INTERVAL = 60000
+DEFAULT_DEBUG = False
+
DISCOVERY_DEVICE = 'device'
DATA_DEVICES = 'zwave_devices'
diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py
index 94de03686d3..2339b8aba36 100644
--- a/homeassistant/components/zwave/node_entity.py
+++ b/homeassistant/components/zwave/node_entity.py
@@ -8,7 +8,7 @@ from homeassistant.helpers.entity import Entity
from .const import (
ATTR_NODE_ID, COMMAND_CLASS_WAKE_UP, ATTR_SCENE_ID, ATTR_SCENE_DATA,
ATTR_BASIC_LEVEL, EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED,
- COMMAND_CLASS_CENTRAL_SCENE)
+ COMMAND_CLASS_CENTRAL_SCENE, DOMAIN)
from .util import node_name, is_node_parsed
_LOGGER = logging.getLogger(__name__)
@@ -110,6 +110,18 @@ class ZWaveNodeEntity(ZWaveBaseEntity):
"""Return unique ID of Z-wave node."""
return self._unique_id
+ @property
+ def device_info(self):
+ """Return device information."""
+ return {
+ 'identifiers': {
+ (DOMAIN, self.node_id)
+ },
+ 'manufacturer': self.node.manufacturer_name,
+ 'model': self.node.product_name,
+ 'name': node_name(self.node)
+ }
+
def network_node_changed(self, node=None, value=None, args=None):
"""Handle a changed node on the network."""
if node and node.node_id != self.node_id:
diff --git a/homeassistant/components/zwave/strings.json b/homeassistant/components/zwave/strings.json
new file mode 100644
index 00000000000..0ac55e46791
--- /dev/null
+++ b/homeassistant/components/zwave/strings.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "title": "Z-Wave",
+ "step": {
+ "user": {
+ "title": "Set up Z-Wave",
+ "description": "See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables",
+ "data": {
+ "usb_path": "USB Path",
+ "network_key": "Network Key (leave blank to auto-generate)"
+ }
+ }
+ },
+ "error": {
+ "option_error": "Z-Wave validation failed. Is the path to the USB stick correct?"
+ },
+ "abort": {
+ "already_configured": "Z-Wave is already configured",
+ "one_instance_only": "Component only supports one Z-Wave instance"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/config.py b/homeassistant/config.py
index 98857d8a83d..8f0690a02c0 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -105,8 +105,9 @@ map:
# Track the sun
sun:
-# Weather prediction
+# Sensors
sensor:
+ # Weather prediction
- platform: yr
# Text to speech
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index 1cc2e1362af..c1c0fbbf775 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -136,19 +136,24 @@ HANDLERS = Registry()
# Components that have config flows. In future we will auto-generate this list.
FLOWS = [
'cast',
- 'hangouts',
'deconz',
+ 'hangouts',
'homematicip_cloud',
'hue',
'ifttt',
'ios',
+ 'lifx',
'mqtt',
'nest',
'openuv',
+ 'simplisafe',
+ 'smhi',
'sonos',
'tradfri',
- 'zone',
+ 'unifi',
'upnp',
+ 'zone',
+ 'zwave'
]
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 502a120a33c..5882bd62314 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,8 +1,8 @@
# coding: utf-8
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
-MINOR_VERSION = 80
-PATCH_VERSION = '3'
+MINOR_VERSION = 81
+PATCH_VERSION = '0'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3)
@@ -82,6 +82,7 @@ CONF_FRIENDLY_NAME_TEMPLATE = 'friendly_name_template'
CONF_HEADERS = 'headers'
CONF_HOST = 'host'
CONF_HOSTS = 'hosts'
+CONF_HS = 'hs'
CONF_ICON = 'icon'
CONF_ICON_TEMPLATE = 'icon_template'
CONF_INCLUDE = 'include'
@@ -128,6 +129,7 @@ CONF_SENSOR_TYPE = 'sensor_type'
CONF_SENSORS = 'sensors'
CONF_SHOW_ON_MAP = 'show_on_map'
CONF_SLAVE = 'slave'
+CONF_SOURCE = 'source'
CONF_SSL = 'ssl'
CONF_STATE = 'state'
CONF_STATE_TEMPLATE = 'state_template'
@@ -145,6 +147,7 @@ CONF_URL = 'url'
CONF_USERNAME = 'username'
CONF_VALUE_TEMPLATE = 'value_template'
CONF_VERIFY_SSL = 'verify_ssl'
+CONF_WEBHOOK_ID = 'webhook_id'
CONF_WEEKDAY = 'weekday'
CONF_WHITELIST = 'whitelist'
CONF_WHITELIST_EXTERNAL_DIRS = 'whitelist_external_dirs'
@@ -166,6 +169,7 @@ EVENT_SERVICE_REGISTERED = 'service_registered'
EVENT_SERVICE_REMOVED = 'service_removed'
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
EVENT_THEMES_UPDATED = 'themes_updated'
+EVENT_TIMER_OUT_OF_SYNC = 'timer_out_of_sync'
# #### DEVICE CLASSES ####
DEVICE_CLASS_BATTERY = 'battery'
@@ -214,6 +218,7 @@ ATTR_CREDENTIALS = 'credentials'
ATTR_NOW = 'now'
ATTR_DATE = 'date'
ATTR_TIME = 'time'
+ATTR_SECONDS = 'seconds'
# Contains domain, service for a SERVICE_CALL event
ATTR_DOMAIN = 'domain'
diff --git a/homeassistant/core.py b/homeassistant/core.py
index d1f811502e0..1754a8b5014 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -29,11 +29,11 @@ from voluptuous.humanize import humanize_error
from homeassistant.const import (
ATTR_DOMAIN, ATTR_FRIENDLY_NAME, ATTR_NOW, ATTR_SERVICE,
- ATTR_SERVICE_CALL_ID, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE,
+ ATTR_SERVICE_CALL_ID, ATTR_SERVICE_DATA, ATTR_SECONDS, EVENT_CALL_SERVICE,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
+ EVENT_HOMEASSISTANT_CLOSE, EVENT_SERVICE_REMOVED,
EVENT_SERVICE_EXECUTED, EVENT_SERVICE_REGISTERED, EVENT_STATE_CHANGED,
- EVENT_TIME_CHANGED, MATCH_ALL, EVENT_HOMEASSISTANT_CLOSE,
- EVENT_SERVICE_REMOVED, __version__)
+ EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, MATCH_ALL, __version__)
from homeassistant import loader
from homeassistant.exceptions import (
HomeAssistantError, InvalidEntityFormatError, InvalidStateError)
@@ -1297,8 +1297,11 @@ def _async_create_timer(hass: HomeAssistant) -> None:
hass.bus.async_fire(EVENT_TIME_CHANGED,
{ATTR_NOW: now})
- if monotonic() > target + 1:
- _LOGGER.error('Timer got out of sync. Resetting')
+ # If we are more than a second late, a tick was missed
+ late = monotonic() - target
+ if late > 1:
+ hass.bus.async_fire(EVENT_TIMER_OUT_OF_SYNC,
+ {ATTR_SECONDS: late})
schedule_tick(now)
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index bb4dcf6a55f..9ce4b6b166d 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -335,9 +335,12 @@ def slugify(value):
def string(value: Any) -> str:
"""Coerce value to string, except for None."""
- if value is not None:
- return str(value)
- raise vol.Invalid('string value is None')
+ if value is None:
+ raise vol.Invalid('string value is None')
+ if isinstance(value, (list, dict)):
+ raise vol.Invalid('value should be a string')
+
+ return str(value)
def temperature_unit(value) -> str:
diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py
index 60fd661a765..687ed0b6f8b 100644
--- a/homeassistant/helpers/entity.py
+++ b/homeassistant/helpers/entity.py
@@ -346,7 +346,7 @@ class Entity:
if hasattr(self, 'async_update'):
await self.async_update()
elif hasattr(self, 'update'):
- await self.hass.async_add_job(self.update)
+ await self.hass.async_add_executor_job(self.update)
finally:
self._update_staged = False
if warning:
@@ -363,14 +363,16 @@ class Entity:
async def async_remove(self):
"""Remove entity from Home Assistant."""
+ will_remove = getattr(self, 'async_will_remove_from_hass', None)
+
+ if will_remove:
+ await will_remove() # pylint: disable=not-callable
+
if self._on_remove is not None:
while self._on_remove:
self._on_remove.pop()()
- if self.platform is not None:
- await self.platform.async_remove_entity(self.entity_id)
- else:
- self.hass.states.async_remove(self.entity_id)
+ self.hass.states.async_remove(self.entity_id)
@callback
def async_registry_updated(self, old, new):
diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py
index c2ab8722c97..982c92510a9 100644
--- a/homeassistant/helpers/entity_component.py
+++ b/homeassistant/helpers/entity_component.py
@@ -2,6 +2,7 @@
import asyncio
from datetime import timedelta
from itertools import chain
+import logging
from homeassistant import config as conf_util
from homeassistant.setup import async_prepare_setup_platform
@@ -11,10 +12,33 @@ from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.service import extract_entity_ids
+from homeassistant.loader import bind_hass
from homeassistant.util import slugify
from .entity_platform import EntityPlatform
DEFAULT_SCAN_INTERVAL = timedelta(seconds=15)
+DATA_INSTANCES = 'entity_components'
+
+
+@bind_hass
+async def async_update_entity(hass, entity_id):
+ """Trigger an update for an entity."""
+ domain = entity_id.split('.', 1)[0]
+ entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain)
+
+ if entity_comp is None:
+ logging.getLogger(__name__).warning(
+ 'Forced update failed. Component for %s not loaded.', entity_id)
+ return
+
+ entity = entity_comp.get_entity(entity_id)
+
+ if entity is None:
+ logging.getLogger(__name__).warning(
+ 'Forced update failed. Entity %s not found.', entity_id)
+ return
+
+ await entity.async_update_ha_state(True)
class EntityComponent:
@@ -45,6 +69,8 @@ class EntityComponent:
self.async_add_entities = self._platforms[domain].async_add_entities
self.add_entities = self._platforms[domain].add_entities
+ hass.data.setdefault(DATA_INSTANCES, {})[domain] = self
+
@property
def entities(self):
"""Return an iterable that returns all entities."""
diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py
index 3ab45577236..5fd580a33f0 100644
--- a/homeassistant/helpers/entity_platform.py
+++ b/homeassistant/helpers/entity_platform.py
@@ -345,8 +345,10 @@ class EntityPlatform:
raise HomeAssistantError(
msg)
- self.entities[entity.entity_id] = entity
- component_entities.add(entity.entity_id)
+ entity_id = entity.entity_id
+ self.entities[entity_id] = entity
+ component_entities.add(entity_id)
+ entity.async_on_remove(lambda: self.entities.pop(entity_id))
if hasattr(entity, 'async_added_to_hass'):
await entity.async_added_to_hass()
@@ -365,7 +367,7 @@ class EntityPlatform:
if not self.entities:
return
- tasks = [self._async_remove_entity(entity_id)
+ tasks = [self.async_remove_entity(entity_id)
for entity_id in self.entities]
await asyncio.wait(tasks, loop=self.hass.loop)
@@ -376,7 +378,7 @@ class EntityPlatform:
async def async_remove_entity(self, entity_id):
"""Remove entity id from platform."""
- await self._async_remove_entity(entity_id)
+ await self.entities[entity_id].async_remove()
# Clean up polling job if no longer needed
if (self._async_unsub_polling is not None and
@@ -385,15 +387,6 @@ class EntityPlatform:
self._async_unsub_polling()
self._async_unsub_polling = None
- async def _async_remove_entity(self, entity_id):
- """Remove entity id from platform."""
- entity = self.entities.pop(entity_id)
-
- if hasattr(entity, 'async_will_remove_from_hass'):
- await entity.async_will_remove_from_hass()
-
- self.hass.states.async_remove(entity_id)
-
async def _update_entity_states(self, now):
"""Update the states of all the polling entities.
diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py
index 4a5daa182fa..5adf748dc58 100644
--- a/homeassistant/helpers/entity_registry.py
+++ b/homeassistant/helpers/entity_registry.py
@@ -185,7 +185,7 @@ class EntityRegistry:
for listener_ref in new.update_listeners:
listener = listener_ref()
if listener is None:
- to_remove.append(listener)
+ to_remove.append(listener_ref)
else:
try:
listener.async_registry_updated(old, new)
diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py
index 05555e8b5c6..1c28e2878e9 100644
--- a/homeassistant/helpers/event.py
+++ b/homeassistant/helpers/event.py
@@ -322,13 +322,13 @@ track_sunset = threaded_listener_factory(async_track_sunset)
@callback
@bind_hass
-def async_track_utc_time_change(hass, action, year=None, month=None, day=None,
+def async_track_utc_time_change(hass, action,
hour=None, minute=None, second=None,
local=False):
"""Add a listener that will fire if time matches a pattern."""
# We do not have to wrap the function with time pattern matching logic
# if no pattern given
- if all(val is None for val in (year, month, day, hour, minute, second)):
+ if all(val is None for val in (hour, minute, second)):
@callback
def time_change_listener(event):
"""Fire every time event that comes in."""
@@ -336,24 +336,45 @@ def async_track_utc_time_change(hass, action, year=None, month=None, day=None,
return hass.bus.async_listen(EVENT_TIME_CHANGED, time_change_listener)
- pmp = _process_time_match
- year, month, day = pmp(year), pmp(month), pmp(day)
- hour, minute, second = pmp(hour), pmp(minute), pmp(second)
+ matching_seconds = dt_util.parse_time_expression(second, 0, 59)
+ matching_minutes = dt_util.parse_time_expression(minute, 0, 59)
+ matching_hours = dt_util.parse_time_expression(hour, 0, 23)
+
+ next_time = None
+
+ def calculate_next(now):
+ """Calculate and set the next time the trigger should fire."""
+ nonlocal next_time
+
+ localized_now = dt_util.as_local(now) if local else now
+ next_time = dt_util.find_next_time_expression_time(
+ localized_now, matching_seconds, matching_minutes,
+ matching_hours)
+
+ # Make sure rolling back the clock doesn't prevent the timer from
+ # triggering.
+ last_now = None
@callback
def pattern_time_change_listener(event):
"""Listen for matching time_changed events."""
+ nonlocal next_time, last_now
+
now = event.data[ATTR_NOW]
- if local:
- now = dt_util.as_local(now)
+ if last_now is None or now < last_now:
+ # Time rolled back or next time not yet calculated
+ calculate_next(now)
- # pylint: disable=too-many-boolean-expressions
- if second(now.second) and minute(now.minute) and hour(now.hour) and \
- day(now.day) and month(now.month) and year(now.year):
+ last_now = now
- hass.async_run_job(action, now)
+ if next_time <= now:
+ hass.async_run_job(action, event.data[ATTR_NOW])
+ calculate_next(now + timedelta(seconds=1))
+ # We can't use async_track_point_in_utc_time here because it would
+ # break in the case that the system time abruptly jumps backwards.
+ # Our custom last_now logic takes care of resolving that scenario.
return hass.bus.async_listen(EVENT_TIME_CHANGED,
pattern_time_change_listener)
@@ -363,11 +384,10 @@ track_utc_time_change = threaded_listener_factory(async_track_utc_time_change)
@callback
@bind_hass
-def async_track_time_change(hass, action, year=None, month=None, day=None,
- hour=None, minute=None, second=None):
+def async_track_time_change(hass, action, hour=None, minute=None, second=None):
"""Add a listener that will fire if UTC time matches a pattern."""
- return async_track_utc_time_change(hass, action, year, month, day, hour,
- minute, second, local=True)
+ return async_track_utc_time_change(hass, action, hour, minute, second,
+ local=True)
track_time_change = threaded_listener_factory(async_track_time_change)
@@ -383,19 +403,3 @@ def _process_state_match(parameter):
parameter = tuple(parameter)
return lambda state: state in parameter
-
-
-def _process_time_match(parameter):
- """Wrap parameter in a tuple if it is not one and returns it."""
- if parameter is None or parameter == MATCH_ALL:
- return lambda _: True
-
- if isinstance(parameter, str) and parameter.startswith('/'):
- parameter = float(parameter[1:])
- return lambda time: time % parameter == 0
-
- if isinstance(parameter, str) or not hasattr(parameter, '__iter__'):
- return lambda time: time == parameter
-
- parameter = tuple(parameter)
- return lambda time: time in parameter
diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index c68aa311998..4650a4d92c2 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -367,18 +367,9 @@ class TemplateMethods:
while to_process:
value = to_process.pop(0)
+ point_state = self._resolve_state(value)
- if isinstance(value, State):
- latitude = value.attributes.get(ATTR_LATITUDE)
- longitude = value.attributes.get(ATTR_LONGITUDE)
-
- if latitude is None or longitude is None:
- _LOGGER.warning(
- "Distance:State does not contains a location: %s",
- value)
- return None
-
- else:
+ if point_state is None:
# We expect this and next value to be lat&lng
if not to_process:
_LOGGER.warning(
@@ -395,6 +386,22 @@ class TemplateMethods:
"longitude: %s, %s", value, value_2)
return None
+ else:
+ if not loc_helper.has_location(point_state):
+ _LOGGER.warning(
+ "distance:State does not contain valid location: %s",
+ point_state)
+ return None
+
+ latitude = point_state.attributes.get(ATTR_LATITUDE)
+ longitude = point_state.attributes.get(ATTR_LONGITUDE)
+
+ if latitude is None or longitude is None:
+ _LOGGER.warning(
+ "Distance:State does not contains a location: %s",
+ value)
+ return None
+
locations.append((latitude, longitude))
if len(locations) == 1:
diff --git a/homeassistant/monkey_patch.py b/homeassistant/monkey_patch.py
index edd25817f5a..52efa586c7f 100644
--- a/homeassistant/monkey_patch.py
+++ b/homeassistant/monkey_patch.py
@@ -25,7 +25,7 @@ from typing import Any
def patch_weakref_tasks() -> None:
"""Replace weakref.WeakSet to address Python 3 bug."""
- # pylint: disable=no-self-use, protected-access, bare-except
+ # pylint: disable=no-self-use, protected-access
import asyncio.tasks
class IgnoreCalls:
@@ -38,7 +38,7 @@ def patch_weakref_tasks() -> None:
asyncio.tasks.Task._all_tasks = IgnoreCalls() # type: ignore
try:
del asyncio.tasks.Task.__del__
- except: # noqa: E722
+ except: # noqa: E722 pylint: disable=bare-except
pass
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 9217e3b3961..fa0d675f4b1 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -1,6 +1,6 @@
aiohttp==3.4.4
astral==1.6.1
-async_timeout==3.0.0
+async_timeout==3.0.1
attrs==18.2.0
bcrypt==3.1.4
certifi>=2018.04.16
diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py
index 04456b8cb2f..0185128abac 100644
--- a/homeassistant/util/async_.py
+++ b/homeassistant/util/async_.py
@@ -145,8 +145,7 @@ def run_coroutine_threadsafe(
"""Handle the call to the coroutine."""
try:
_chain_future(ensure_future(coro, loop=loop), future)
- # pylint: disable=broad-except
- except Exception as exc:
+ except Exception as exc: # pylint: disable=broad-except
if future.set_running_or_notify_cancel():
future.set_exception(exc)
else:
@@ -194,8 +193,7 @@ def run_callback_threadsafe(loop: AbstractEventLoop, callback: Callable,
"""Run callback and store result."""
try:
future.set_result(callback(*args))
- # pylint: disable=broad-except
- except Exception as exc:
+ except Exception as exc: # pylint: disable=broad-except
if future.set_running_or_notify_cancel():
future.set_exception(exc)
else:
diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py
index 5d4b10454a7..b3f7cdd434c 100644
--- a/homeassistant/util/dt.py
+++ b/homeassistant/util/dt.py
@@ -1,10 +1,14 @@
"""Helper methods to handle the time in Home Assistant."""
import datetime as dt
import re
-from typing import Any, Dict, Union, Optional, Tuple # noqa pylint: disable=unused-import
+from typing import (Any, Union, Optional, # noqa pylint: disable=unused-import
+ Tuple, List, cast, Dict)
import pytz
import pytz.exceptions as pytzexceptions
+import pytz.tzinfo as pytzinfo # noqa pylint: disable=unused-import
+
+from homeassistant.const import MATCH_ALL
DATE_STR_FORMAT = "%Y-%m-%d"
UTC = pytz.utc
@@ -209,3 +213,162 @@ def get_age(date: dt.datetime) -> str:
return formatn(minute, 'minute')
return formatn(second, 'second')
+
+
+def parse_time_expression(parameter: Any, min_value: int, max_value: int) \
+ -> List[int]:
+ """Parse the time expression part and return a list of times to match."""
+ if parameter is None or parameter == MATCH_ALL:
+ res = [x for x in range(min_value, max_value + 1)]
+ elif isinstance(parameter, str) and parameter.startswith('/'):
+ parameter = float(parameter[1:])
+ res = [x for x in range(min_value, max_value + 1)
+ if x % parameter == 0]
+ elif not hasattr(parameter, '__iter__'):
+ res = [int(parameter)]
+ else:
+ res = list(sorted(int(x) for x in parameter))
+
+ for val in res:
+ if val < min_value or val > max_value:
+ raise ValueError(
+ "Time expression '{}': parameter {} out of range ({} to {})"
+ "".format(parameter, val, min_value, max_value)
+ )
+
+ return res
+
+
+# pylint: disable=redefined-outer-name
+def find_next_time_expression_time(now: dt.datetime,
+ seconds: List[int], minutes: List[int],
+ hours: List[int]) -> dt.datetime:
+ """Find the next datetime from now for which the time expression matches.
+
+ The algorithm looks at each time unit separately and tries to find the
+ next one that matches for each. If any of them would roll over, all
+ time units below that are reset to the first matching value.
+
+ Timezones are also handled (the tzinfo of the now object is used),
+ including daylight saving time.
+ """
+ if not seconds or not minutes or not hours:
+ raise ValueError("Cannot find a next time: Time expression never "
+ "matches!")
+
+ def _lower_bound(arr: List[int], cmp: int) -> Optional[int]:
+ """Return the first value in arr greater or equal to cmp.
+
+ Return None if no such value exists.
+ """
+ left = 0
+ right = len(arr)
+ while left < right:
+ mid = (left + right) // 2
+ if arr[mid] < cmp:
+ left = mid + 1
+ else:
+ right = mid
+
+ if left == len(arr):
+ return None
+ return arr[left]
+
+ result = now.replace(microsecond=0)
+
+ # Match next second
+ next_second = _lower_bound(seconds, result.second)
+ if next_second is None:
+ # No second to match in this minute. Roll-over to next minute.
+ next_second = seconds[0]
+ result += dt.timedelta(minutes=1)
+
+ result = result.replace(second=next_second)
+
+ # Match next minute
+ next_minute = _lower_bound(minutes, result.minute)
+ if next_minute != result.minute:
+ # We're in the next minute. Seconds needs to be reset.
+ result = result.replace(second=seconds[0])
+
+ if next_minute is None:
+ # No minute to match in this hour. Roll-over to next hour.
+ next_minute = minutes[0]
+ result += dt.timedelta(hours=1)
+
+ result = result.replace(minute=next_minute)
+
+ # Match next hour
+ next_hour = _lower_bound(hours, result.hour)
+ if next_hour != result.hour:
+ # We're in the next hour. Seconds+minutes needs to be reset.
+ result.replace(second=seconds[0], minute=minutes[0])
+
+ if next_hour is None:
+ # No minute to match in this day. Roll-over to next day.
+ next_hour = hours[0]
+ result += dt.timedelta(days=1)
+
+ result = result.replace(hour=next_hour)
+
+ if result.tzinfo is None:
+ return result
+
+ # Now we need to handle timezones. We will make this datetime object
+ # "naive" first and then re-convert it to the target timezone.
+ # This is so that we can call pytz's localize and handle DST changes.
+ tzinfo = result.tzinfo # type: pytzinfo.DstTzInfo
+ result = result.replace(tzinfo=None)
+
+ try:
+ result = tzinfo.localize(result, is_dst=None)
+ except pytzexceptions.AmbiguousTimeError:
+ # This happens when we're leaving daylight saving time and local
+ # clocks are rolled back. In this case, we want to trigger
+ # on both the DST and non-DST time. So when "now" is in the DST
+ # use the DST-on time, and if not, use the DST-off time.
+ use_dst = bool(now.dst())
+ result = tzinfo.localize(result, is_dst=use_dst)
+ except pytzexceptions.NonExistentTimeError:
+ # This happens when we're entering daylight saving time and local
+ # clocks are rolled forward, thus there are local times that do
+ # not exist. In this case, we want to trigger on the next time
+ # that *does* exist.
+ # In the worst case, this will run through all the seconds in the
+ # time shift, but that's max 3600 operations for once per year
+ result = result.replace(tzinfo=tzinfo) + dt.timedelta(seconds=1)
+ return find_next_time_expression_time(result, seconds, minutes, hours)
+
+ result_dst = cast(dt.timedelta, result.dst())
+ now_dst = cast(dt.timedelta, now.dst())
+ if result_dst >= now_dst:
+ return result
+
+ # Another edge-case when leaving DST:
+ # When now is in DST and ambiguous *and* the next trigger time we *should*
+ # trigger is ambiguous and outside DST, the excepts above won't catch it.
+ # For example: if triggering on 2:30 and now is 28.10.2018 2:30 (in DST)
+ # we should trigger next on 28.10.2018 2:30 (out of DST), but our
+ # algorithm above would produce 29.10.2018 2:30 (out of DST)
+
+ # Step 1: Check if now is ambiguous
+ try:
+ tzinfo.localize(now.replace(tzinfo=None), is_dst=None)
+ return result
+ except pytzexceptions.AmbiguousTimeError:
+ pass
+
+ # Step 2: Check if result of (now - DST) is ambiguous.
+ check = now - now_dst
+ check_result = find_next_time_expression_time(
+ check, seconds, minutes, hours)
+ try:
+ tzinfo.localize(check_result.replace(tzinfo=None), is_dst=None)
+ return result
+ except pytzexceptions.AmbiguousTimeError:
+ pass
+
+ # OK, edge case does apply. We must override the DST to DST-off
+ check_result = tzinfo.localize(check_result.replace(tzinfo=None),
+ is_dst=False)
+ return check_result
diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py
index 0a2a2a1edf3..b002c8e3147 100644
--- a/homeassistant/util/json.py
+++ b/homeassistant/util/json.py
@@ -4,7 +4,7 @@ from typing import Union, List, Dict
import json
import os
-from os import O_WRONLY, O_CREAT, O_TRUNC
+import tempfile
from homeassistant.exceptions import HomeAssistantError
@@ -46,13 +46,17 @@ def save_json(filename: str, data: Union[List, Dict],
Returns True on success.
"""
- tmp_filename = filename + "__TEMP__"
+ tmp_filename = ""
+ tmp_path = os.path.split(filename)[0]
try:
json_data = json.dumps(data, sort_keys=True, indent=4)
- mode = 0o600 if private else 0o644
- with open(os.open(tmp_filename, O_WRONLY | O_CREAT | O_TRUNC, mode),
- 'w', encoding='utf-8') as fdesc:
+ # Modern versions of Python tempfile create this file with mode 0o600
+ with tempfile.NamedTemporaryFile(mode="w", encoding='utf-8',
+ dir=tmp_path, delete=False) as fdesc:
fdesc.write(json_data)
+ tmp_filename = fdesc.name
+ if not private:
+ os.chmod(tmp_filename, 0o644)
os.replace(tmp_filename, filename)
except TypeError as error:
_LOGGER.exception('Failed to serialize to JSON: %s',
diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py
index 5a8f515c3ad..5f6d202b5e9 100644
--- a/homeassistant/util/unit_system.py
+++ b/homeassistant/util/unit_system.py
@@ -13,6 +13,7 @@ from homeassistant.const import (
TEMPERATURE, UNIT_NOT_RECOGNIZED_TEMPLATE)
from homeassistant.util import temperature as temperature_util
from homeassistant.util import distance as distance_util
+from homeassistant.util import volume as volume_util
_LOGGER = logging.getLogger(__name__)
@@ -108,6 +109,13 @@ class UnitSystem:
return distance_util.convert(length, from_unit,
self.length_unit)
+ def volume(self, volume: Optional[float], from_unit: str) -> float:
+ """Convert the given volume to this unit system."""
+ if not isinstance(volume, Number):
+ raise TypeError('{} is not a numeric value.'.format(str(volume)))
+
+ return volume_util.convert(volume, from_unit, self.volume_unit)
+
def as_dict(self) -> dict:
"""Convert the unit system to a dictionary."""
return {
diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py
new file mode 100644
index 00000000000..154fb3d2c8b
--- /dev/null
+++ b/homeassistant/util/volume.py
@@ -0,0 +1,45 @@
+"""Volume conversion util functions."""
+
+import logging
+from numbers import Number
+from homeassistant.const import (VOLUME_LITERS, VOLUME_MILLILITERS,
+ VOLUME_GALLONS, VOLUME_FLUID_OUNCE,
+ VOLUME, UNIT_NOT_RECOGNIZED_TEMPLATE)
+
+_LOGGER = logging.getLogger(__name__)
+
+VALID_UNITS = [VOLUME_LITERS, VOLUME_MILLILITERS, VOLUME_GALLONS,
+ VOLUME_FLUID_OUNCE]
+
+
+def __liter_to_gallon(liter: float) -> float:
+ """Convert a volume measurement in Liter to Gallon."""
+ return liter * .2642
+
+
+def __gallon_to_liter(gallon: float) -> float:
+ """Convert a volume measurement in Gallon to Liter."""
+ return gallon * 3.785
+
+
+def convert(volume: float, from_unit: str, to_unit: str) -> float:
+ """Convert a temperature from one unit to another."""
+ if from_unit not in VALID_UNITS:
+ raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit,
+ VOLUME))
+ if to_unit not in VALID_UNITS:
+ raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, VOLUME))
+
+ if not isinstance(volume, Number):
+ raise TypeError('{} is not of numeric type'.format(volume))
+
+ if from_unit == to_unit:
+ return volume
+
+ result = volume
+ if from_unit == VOLUME_LITERS and to_unit == VOLUME_GALLONS:
+ result = __liter_to_gallon(volume)
+ elif from_unit == VOLUME_GALLONS and to_unit == VOLUME_LITERS:
+ result = __gallon_to_liter(volume)
+
+ return result
diff --git a/requirements_all.txt b/requirements_all.txt
index a088fb696d3..ecb09679d42 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1,7 +1,7 @@
# Home Assistant core
aiohttp==3.4.4
astral==1.6.1
-async_timeout==3.0.0
+async_timeout==3.0.1
attrs==18.2.0
bcrypt==3.1.4
certifi>=2018.04.16
@@ -34,7 +34,7 @@ Adafruit-SHT31==1.0.2
DoorBirdPy==0.1.3
# homeassistant.components.homekit
-HAP-python==2.2.2
+HAP-python==2.3.0
# homeassistant.components.notify.mastodon
Mastodon.py==1.3.1
@@ -52,13 +52,16 @@ PyMata==2.14
PyQRCode==1.2.1
# homeassistant.components.sensor.rmvtransport
-PyRMVtransport==0.1
+PyRMVtransport==0.1.3
# homeassistant.components.switch.switchbot
PySwitchbot==0.3
+# homeassistant.components.sensor.transport_nsw
+PyTransportNSW==0.0.8
+
# homeassistant.components.xiaomi_aqara
-PyXiaomiGateway==0.11.0
+PyXiaomiGateway==0.11.1
# homeassistant.components.rpi_gpio
# RPi.GPIO==0.6.1
@@ -79,7 +82,7 @@ WazeRouteCalculator==0.6
YesssSMS==0.2.3
# homeassistant.components.abode
-abodepy==0.13.1
+abodepy==0.14.0
# homeassistant.components.media_player.frontier_silicon
afsapi==0.0.4
@@ -106,8 +109,8 @@ aiohue==1.5.0
# homeassistant.components.sensor.imap
aioimaplib==0.7.13
-# homeassistant.components.light.lifx
-aiolifx==0.6.3
+# homeassistant.components.lifx
+aiolifx==0.6.5
# homeassistant.components.light.lifx
aiolifx_effects==0.2.1
@@ -115,6 +118,9 @@ aiolifx_effects==0.2.1
# homeassistant.components.scene.hunterdouglas_powerview
aiopvapi==1.5.4
+# homeassistant.components.unifi
+aiounifi==3
+
# homeassistant.components.cover.aladdin_connect
aladdin_connect==0.3
@@ -147,7 +153,7 @@ asterisk_mbox==0.5.0
# homeassistant.components.upnp
# homeassistant.components.media_player.dlna_dmr
-async-upnp-client==0.12.6
+async-upnp-client==0.12.7
# homeassistant.components.light.avion
# avion==0.7
@@ -179,7 +185,7 @@ bellows==0.7.0
bimmer_connected==0.5.3
# homeassistant.components.blink
-blinkpy==0.10.0
+blinkpy==0.10.1
# homeassistant.components.light.blinksticklight
blinkstick==1.1.8
@@ -196,11 +202,12 @@ blockchain==1.4.4
# homeassistant.components.sensor.bme680
# bme680==1.0.4
+# homeassistant.components.route53
# homeassistant.components.notify.aws_lambda
# homeassistant.components.notify.aws_sns
# homeassistant.components.notify.aws_sqs
# homeassistant.components.tts.amazon_polly
-boto3==1.4.7
+boto3==1.9.16
# homeassistant.scripts.credstash
botocore==1.7.34
@@ -253,7 +260,7 @@ concord232==0.15
# homeassistant.components.sensor.xiaomi_miio
# homeassistant.components.switch.xiaomi_miio
# homeassistant.components.vacuum.xiaomi_miio
-construct==2.9.41
+construct==2.9.45
# homeassistant.scripts.credstash
# credstash==1.14.0
@@ -282,7 +289,7 @@ defusedxml==0.5.0
deluge-client==1.4.0
# homeassistant.components.media_player.denonavr
-denonavr==0.7.5
+denonavr==0.7.6
# homeassistant.components.media_player.directv
directpy==0.5
@@ -299,14 +306,11 @@ distro==1.3.0
# homeassistant.components.switch.digitalloggers
dlipower==0.7.165
-# homeassistant.components.notify.xmpp
-dnspython3==1.15.0
-
# homeassistant.components.sensor.dovado
dovado==0.4.1
# homeassistant.components.sensor.dsmr
-dsmr_parser==0.11
+dsmr_parser==0.12
# homeassistant.components.dweet
# homeassistant.components.sensor.dweet
@@ -321,6 +325,9 @@ einder==0.3.1
# homeassistant.components.sensor.eliqonline
eliqonline==1.0.14
+# homeassistant.components.elkm1
+elkm1-lib==0.7.10
+
# homeassistant.components.enocean
enocean==0.40
@@ -360,7 +367,7 @@ fedexdeliverymanager==1.0.6
feedparser==5.2.1
# homeassistant.components.sensor.fints
-fints==0.2.1
+fints==1.0.1
# homeassistant.components.sensor.fitbit
fitbit==0.3.0
@@ -369,7 +376,7 @@ fitbit==0.3.0
fixerio==1.0.0a0
# homeassistant.components.light.flux_led
-flux_led==0.21
+flux_led==0.22
# homeassistant.components.sensor.foobot
foobot_async==0.3.1
@@ -395,6 +402,7 @@ gearbest_parser==1.0.7
geizhals==0.0.7
# homeassistant.components.geo_location.geo_json_events
+# homeassistant.components.geo_location.nsw_rural_fire_service_feed
geojson_client==0.1
# homeassistant.components.sensor.geo_rss_events
@@ -422,7 +430,7 @@ gps3==0.33.3
greenwavereality==0.5.1
# homeassistant.components.media_player.gstreamer
-gstreamer-player==1.1.0
+gstreamer-player==1.1.2
# homeassistant.components.ffmpeg
ha-ffmpeg==1.9
@@ -440,7 +448,7 @@ hangups==0.4.6
hbmqtt==0.9.4
# homeassistant.components.sensor.jewish_calendar
-hdate==0.6.3
+hdate==0.6.5
# homeassistant.components.climate.heatmiser
heatmiserV3==0.9.1
@@ -455,10 +463,10 @@ hipnotify==1.0.8
hole==0.3.0
# homeassistant.components.binary_sensor.workday
-holidays==0.9.7
+holidays==0.9.8
# homeassistant.components.frontend
-home-assistant-frontend==20181018.0
+home-assistant-frontend==20181026.0
# homeassistant.components.homekit_controller
# homekit==0.10
@@ -495,11 +503,14 @@ ihcsdk==2.2.0
influxdb==5.0.0
# homeassistant.components.insteon
-insteonplm==0.14.2
+insteonplm==0.15.0
# homeassistant.components.sensor.iperf3
iperf3==0.1.10
+# homeassistant.components.route53
+ipify==1.0.0
+
# homeassistant.components.verisure
jsonpath==0.75
@@ -520,7 +531,7 @@ keyrings.alt==3.1
kiwiki-client==0.1.1
# homeassistant.components.konnected
-konnected==0.1.2
+konnected==0.1.4
# homeassistant.components.eufy
lakeside==0.10
@@ -548,7 +559,7 @@ liffylights==0.9.4
lightify==1.0.6.1
# homeassistant.components.light.limitlessled
-limitlessled==1.1.2
+limitlessled==1.1.3
# homeassistant.components.linode
linode-api==4.1.9b1
@@ -561,7 +572,7 @@ liveboxplaytv==2.0.2
lmnotify==0.0.4
# homeassistant.components.device_tracker.google_maps
-locationsharinglib==3.0.3
+locationsharinglib==3.0.6
# homeassistant.components.logi_circle
logi_circle==0.1.7
@@ -594,6 +605,9 @@ mficlient==0.3.0
# homeassistant.components.sensor.miflora
miflora==0.4.0
+# homeassistant.components.climate.mill
+millheater==0.1.2
+
# homeassistant.components.sensor.mitemp_bt
mitemp_bt==0.0.1
@@ -626,7 +640,7 @@ ndms2_client==0.0.4
netdata==0.1.2
# homeassistant.components.discovery
-netdisco==2.1.0
+netdisco==2.2.0
# homeassistant.components.sensor.neurio_energy
neurio==0.3.1
@@ -642,7 +656,7 @@ nuheat==0.3.0
# homeassistant.components.binary_sensor.trend
# homeassistant.components.image_processing.opencv
-numpy==1.15.1
+numpy==1.15.2
# homeassistant.components.google
oauth2client==4.0.0
@@ -759,13 +773,13 @@ pyCEC==0.4.13
pyHS100==0.3.3
# homeassistant.components.weather.met
-pyMetno==0.2.0
+pyMetno==0.3.0
# homeassistant.components.rfxtrx
pyRFXtrx==0.23
# homeassistant.components.switch.switchmate
-pySwitchmate==0.4.1
+pySwitchmate==0.4.2
# homeassistant.components.tibber
pyTibber==0.7.2
@@ -776,9 +790,6 @@ pyW215==0.6.0
# homeassistant.components.sensor.noaa_tides
# py_noaa==0.3.0
-# homeassistant.components.cover.ryobi_gdo
-py_ryobi_gdo==0.0.10
-
# homeassistant.components.ads
pyads==2.2.6
@@ -789,13 +800,7 @@ pyairvisual==2.0.1
pyalarmdotcom==0.3.2
# homeassistant.components.arlo
-pyarlo==0.2.0
-
-# homeassistant.components.notify.xmpp
-pyasn1-modules==0.1.5
-
-# homeassistant.components.notify.xmpp
-pyasn1==0.3.7
+pyarlo==0.2.2
# homeassistant.components.netatmo
pyatmo==1.2
@@ -856,7 +861,7 @@ pydukeenergy==0.0.6
# homeassistant.components.sensor.ebox
pyebox==1.1.4
-# homeassistant.components.climate.econet
+# homeassistant.components.water_heater.econet
pyeconet==0.0.6
# homeassistant.components.switch.edimax
@@ -912,7 +917,7 @@ pyhik==0.1.8
pyhiveapi==0.2.14
# homeassistant.components.homematic
-pyhomematic==0.1.50
+pyhomematic==0.1.51
# homeassistant.components.sensor.hydroquebec
pyhydroquebec==2.2.2
@@ -952,10 +957,10 @@ pylgnetcast-homeassistant==0.2.0.dev0
# homeassistant.components.media_player.webostv
# homeassistant.components.notify.webostv
-pylgtv==0.1.7
+pylgtv==0.1.9
# homeassistant.components.sensor.linky
-pylinky==0.1.6
+pylinky==0.1.8
# homeassistant.components.litejet
pylitejet==0.1
@@ -1000,10 +1005,10 @@ pymysensors==0.17.0
pynello==1.5.1
# homeassistant.components.device_tracker.netgear
-pynetgear==0.4.2
+pynetgear==0.5.0
# homeassistant.components.switch.netio
-pynetio==0.1.6
+pynetio==0.1.9.1
# homeassistant.components.lock.nuki
pynuki==1.3.1
@@ -1024,8 +1029,8 @@ pyoppleio==1.0.5
# homeassistant.components.iota
pyota==2.0.5
-# homeassistant.components.climate.opentherm_gw
-pyotgw==0.1b0
+# homeassistant.components.opentherm_gw
+pyotgw==0.2b1
# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
@@ -1048,8 +1053,11 @@ pyqwikswitch==0.8
# homeassistant.components.rainbird
pyrainbird==0.1.6
+# homeassistant.components.switch.recswitch
+pyrecswitch==1.0.2
+
# homeassistant.components.sabnzbd
-pysabnzbd==1.0.1
+pysabnzbd==1.1.0
# homeassistant.components.climate.sensibo
pysensibo==1.0.3
@@ -1064,7 +1072,7 @@ pyserial==3.1.1
pysesame==0.1.0
# homeassistant.components.goalfeed
-pysher==0.2.0
+pysher==1.0.4
# homeassistant.components.sensor.sma
pysma==0.2
@@ -1144,7 +1152,7 @@ python-juicenet==0.0.5
# homeassistant.components.sensor.xiaomi_miio
# homeassistant.components.switch.xiaomi_miio
# homeassistant.components.vacuum.xiaomi_miio
-python-miio==0.4.1
+python-miio==0.4.2
# homeassistant.components.media_player.mpd
python-mpd2==1.0.0
@@ -1181,7 +1189,7 @@ python-synology==0.2.0
python-tado==0.2.3
# homeassistant.components.telegram_bot
-python-telegram-bot==11.0.0
+python-telegram-bot==11.1.0
# homeassistant.components.sensor.twitch
python-twitch-client==0.6.0
@@ -1199,7 +1207,7 @@ python-wink==1.10.1
python_opendata_transport==0.1.4
# homeassistant.components.zwave
-python_openzwave==0.4.9
+python_openzwave==0.4.10
# homeassistant.components.egardia
pythonegardia==1.0.39
@@ -1232,7 +1240,7 @@ pyuptimerobot==0.0.5
# pyuserinput==0.1.11
# homeassistant.components.vera
-pyvera==0.2.44
+pyvera==0.2.45
# homeassistant.components.switch.vesync
pyvesync==0.1.1
@@ -1258,6 +1266,9 @@ pyzabbix==0.7.4
# homeassistant.components.sensor.qnap
qnapstats==0.2.7
+# homeassistant.components.device_tracker.quantum_gateway
+quantum-gateway==0.0.3
+
# homeassistant.components.rachio
rachiopy==0.1.3
@@ -1294,6 +1305,9 @@ roombapy==1.3.1
# homeassistant.components.switch.rpi_rf
# rpi-rf==0.9.6
+# homeassistant.components.lovelace
+ruamel.yaml==0.15.72
+
# homeassistant.components.media_player.russound_rnet
russound==0.1.9
@@ -1329,13 +1343,13 @@ sense_energy==0.4.2
sharp_aquos_rc==0.3.2
# homeassistant.components.sensor.shodan
-shodan==1.10.2
+shodan==1.10.4
# homeassistant.components.notify.simplepush
simplepush==1.1.4
-# homeassistant.components.alarm_control_panel.simplisafe
-simplisafe-python==3.1.2
+# homeassistant.components.simplisafe
+simplisafe-python==3.1.12
# homeassistant.components.sisyphus
sisyphus-control==2.1
@@ -1346,12 +1360,12 @@ skybellpy==0.1.2
# homeassistant.components.notify.slack
slacker==0.9.65
-# homeassistant.components.notify.xmpp
-sleekxmpp==1.3.2
-
# homeassistant.components.sleepiq
sleepyq==0.6
+# homeassistant.components.notify.xmpp
+slixmpp==1.4.0
+
# homeassistant.components.smappee
smappy==0.2.16
@@ -1363,6 +1377,9 @@ smappy==0.2.16
# homeassistant.components.sensor.htu21d
# smbus-cffi==0.5.1
+# homeassistant.components.smhi
+smhi-pkg==1.0.5
+
# homeassistant.components.media_player.snapcast
snapcast==2.0.9
@@ -1398,6 +1415,9 @@ statsd==3.2.1
# homeassistant.components.sensor.steam_online
steamodd==4.21
+# homeassistant.components.sensor.thermoworks_smoke
+stringcase==1.2.0
+
# homeassistant.components.ecovacs
sucks==0.9.3
@@ -1425,12 +1445,18 @@ tellcore-py==1.1.2
# homeassistant.components.tellduslive
tellduslive==0.10.4
+# homeassistant.components.media_player.lg_soundbar
+temescal==0.1
+
# homeassistant.components.sensor.temper
temperusb==1.5.3
# homeassistant.components.tesla
teslajsonpy==0.0.23
+# homeassistant.components.sensor.thermoworks_smoke
+thermoworks_smoke==0.1.7
+
# homeassistant.components.thingspeak
thingspeak==0.4.1
@@ -1444,7 +1470,7 @@ todoist-python==7.0.17
toonlib==1.0.2
# homeassistant.components.alarm_control_panel.totalconnect
-total_connect_client==0.18
+total_connect_client==0.20
# homeassistant.components.device_tracker.tplink
tplink==0.2.1
@@ -1457,7 +1483,7 @@ transmissionrpc==0.11
tuyapy==0.1.3
# homeassistant.components.twilio
-twilio==5.7.0
+twilio==6.19.1
# homeassistant.components.sensor.uber
uber_rides==0.6.0
@@ -1484,7 +1510,7 @@ volkszaehler==0.1.2
volvooncall==0.4.0
# homeassistant.components.verisure
-vsure==1.3.7
+vsure==1.5.0
# homeassistant.components.sensor.vasttrafik
vtjp==0.1.14
@@ -1547,13 +1573,13 @@ yahooweather==0.10
yalesmartalarmclient==0.1.4
# homeassistant.components.light.yeelight
-yeelight==0.4.0
+yeelight==0.4.3
# homeassistant.components.light.yeelightsunflower
yeelightsunflower==0.0.10
# homeassistant.components.media_extractor
-youtube_dl==2018.09.26
+youtube_dl==2018.10.05
# homeassistant.components.light.zengge
zengge==0.2
@@ -1574,4 +1600,4 @@ zigpy-xbee==0.1.1
zigpy==0.2.0
# homeassistant.components.zoneminder
-zm-py==0.0.5
+zm-py==0.1.0
diff --git a/requirements_test.txt b/requirements_test.txt
index 9f36d8f42ca..492708cd904 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -13,5 +13,5 @@ pytest-aiohttp==0.3.0
pytest-cov==2.5.1
pytest-sugar==0.9.1
pytest-timeout==1.3.2
-pytest==3.8.2
+pytest==3.9.1
requests_mock==1.5.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index b92e5616c3d..7da93c5e0bc 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -14,15 +14,18 @@ pytest-aiohttp==0.3.0
pytest-cov==2.5.1
pytest-sugar==0.9.1
pytest-timeout==1.3.2
-pytest==3.8.2
+pytest==3.9.1
requests_mock==1.5.2
# homeassistant.components.homekit
-HAP-python==2.2.2
+HAP-python==2.3.0
# homeassistant.components.sensor.rmvtransport
-PyRMVtransport==0.1
+PyRMVtransport==0.1.3
+
+# homeassistant.components.sensor.transport_nsw
+PyTransportNSW==0.0.8
# homeassistant.components.notify.yessssms
YesssSMS==0.2.3
@@ -37,6 +40,9 @@ aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==1.5.0
+# homeassistant.components.unifi
+aiounifi==3
+
# homeassistant.components.notify.apns
apns2==0.3.0
@@ -50,7 +56,7 @@ coinmarketcap==5.0.3
defusedxml==0.5.0
# homeassistant.components.sensor.dsmr
-dsmr_parser==0.11
+dsmr_parser==0.12
# homeassistant.components.sensor.season
ephem==3.7.6.0
@@ -69,6 +75,7 @@ foobot_async==0.3.1
gTTS-token==1.1.2
# homeassistant.components.geo_location.geo_json_events
+# homeassistant.components.geo_location.nsw_rural_fire_service_feed
geojson_client==0.1
# homeassistant.components.sensor.geo_rss_events
@@ -84,13 +91,13 @@ hangups==0.4.6
hbmqtt==0.9.4
# homeassistant.components.sensor.jewish_calendar
-hdate==0.6.3
+hdate==0.6.5
# homeassistant.components.binary_sensor.workday
-holidays==0.9.7
+holidays==0.9.8
# homeassistant.components.frontend
-home-assistant-frontend==20181018.0
+home-assistant-frontend==20181026.0
# homeassistant.components.homematicip_cloud
homematicip==0.9.8
@@ -111,7 +118,7 @@ mficlient==0.3.0
# homeassistant.components.binary_sensor.trend
# homeassistant.components.image_processing.opencv
-numpy==1.15.1
+numpy==1.15.2
# homeassistant.components.mqtt
# homeassistant.components.shiftr
@@ -150,6 +157,9 @@ pydeconz==47
# homeassistant.components.zwave
pydispatcher==2.0.5
+# homeassistant.components.homematic
+pyhomematic==0.1.51
+
# homeassistant.components.litejet
pylitejet==0.1
@@ -205,12 +215,21 @@ rflink==0.0.37
# homeassistant.components.ring
ring_doorbell==0.2.1
+# homeassistant.components.lovelace
+ruamel.yaml==0.15.72
+
# homeassistant.components.media_player.yamaha
rxv==0.5.1
+# homeassistant.components.simplisafe
+simplisafe-python==3.1.12
+
# homeassistant.components.sleepiq
sleepyq==0.6
+# homeassistant.components.smhi
+smhi-pkg==1.0.5
+
# homeassistant.components.climate.honeywell
somecomfort==0.5.2
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index 9c0323bf5ca..491531ee12b 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -40,6 +40,7 @@ TEST_REQUIREMENTS = (
'aioautomatic',
'aiohttp_cors',
'aiohue',
+ 'aiounifi',
'apns2',
'caldav',
'coinmarketcap',
@@ -76,6 +77,7 @@ TEST_REQUIREMENTS = (
'pyblackbird',
'pydeconz',
'pydispatcher',
+ 'pyhomematic',
'pylitejet',
'pymonoprice',
'pynx584',
@@ -84,6 +86,7 @@ TEST_REQUIREMENTS = (
'pysonos',
'pyqwikswitch',
'PyRMVtransport',
+ 'PyTransportNSW',
'pyspcwebgw',
'python-forecastio',
'python-nest',
@@ -95,7 +98,9 @@ TEST_REQUIREMENTS = (
'rflink',
'ring_doorbell',
'rxv',
+ 'simplisafe-python',
'sleepyq',
+ 'smhi-pkg',
'somecomfort',
'sqlalchemy',
'statsd',
@@ -106,6 +111,7 @@ TEST_REQUIREMENTS = (
'wakeonlan',
'vultr',
'YesssSMS',
+ 'ruamel.yaml',
)
IGNORE_PACKAGES = (
diff --git a/setup.py b/setup.py
index 727badb1d94..90f2e8357fd 100755
--- a/setup.py
+++ b/setup.py
@@ -34,7 +34,7 @@ PACKAGES = find_packages(exclude=['tests', 'tests.*'])
REQUIRES = [
'aiohttp==3.4.4',
'astral==1.6.1',
- 'async_timeout==3.0.0',
+ 'async_timeout==3.0.1',
'attrs==18.2.0',
'bcrypt==3.1.4',
'certifi>=2018.04.16',
diff --git a/tests/auth/test_auth_store.py b/tests/auth/test_auth_store.py
new file mode 100644
index 00000000000..a3bdbab93d7
--- /dev/null
+++ b/tests/auth/test_auth_store.py
@@ -0,0 +1,82 @@
+"""Tests for the auth store."""
+from homeassistant.auth import auth_store
+
+
+async def test_loading_old_data_format(hass, hass_storage):
+ """Test we correctly load an old data format."""
+ hass_storage[auth_store.STORAGE_KEY] = {
+ 'version': 1,
+ 'data': {
+ 'credentials': [],
+ 'users': [
+ {
+ "id": "user-id",
+ "is_active": True,
+ "is_owner": True,
+ "name": "Paulus",
+ "system_generated": False,
+ },
+ {
+ "id": "system-id",
+ "is_active": True,
+ "is_owner": True,
+ "name": "Hass.io",
+ "system_generated": True,
+ }
+ ],
+ "refresh_tokens": [
+ {
+ "access_token_expiration": 1800.0,
+ "client_id": "http://localhost:8123/",
+ "created_at": "2018-10-03T13:43:19.774637+00:00",
+ "id": "user-token-id",
+ "jwt_key": "some-key",
+ "last_used_at": "2018-10-03T13:43:19.774712+00:00",
+ "token": "some-token",
+ "user_id": "user-id"
+ },
+ {
+ "access_token_expiration": 1800.0,
+ "client_id": None,
+ "created_at": "2018-10-03T13:43:19.774637+00:00",
+ "id": "system-token-id",
+ "jwt_key": "some-key",
+ "last_used_at": "2018-10-03T13:43:19.774712+00:00",
+ "token": "some-token",
+ "user_id": "system-id"
+ },
+ {
+ "access_token_expiration": 1800.0,
+ "client_id": "http://localhost:8123/",
+ "created_at": "2018-10-03T13:43:19.774637+00:00",
+ "id": "hidden-because-no-jwt-id",
+ "last_used_at": "2018-10-03T13:43:19.774712+00:00",
+ "token": "some-token",
+ "user_id": "user-id"
+ },
+ ]
+ }
+ }
+
+ store = auth_store.AuthStore(hass)
+ groups = await store.async_get_groups()
+ assert len(groups) == 1
+ group = groups[0]
+ assert group.name == "All Access"
+
+ users = await store.async_get_users()
+ assert len(users) == 2
+
+ owner, system = users
+
+ assert owner.system_generated is False
+ assert owner.groups == [group]
+ assert len(owner.refresh_tokens) == 1
+ owner_token = list(owner.refresh_tokens.values())[0]
+ assert owner_token.id == 'user-token-id'
+
+ assert system.system_generated is True
+ assert system.groups == []
+ assert len(system.refresh_tokens) == 1
+ system_token = list(system.refresh_tokens.values())[0]
+ assert system_token.id == 'system-token-id'
diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py
index 8fd9b8930e4..4357ba1b1de 100644
--- a/tests/auth/test_init.py
+++ b/tests/auth/test_init.py
@@ -10,6 +10,7 @@ from homeassistant import auth, data_entry_flow
from homeassistant.auth import (
models as auth_models, auth_store, const as auth_const)
from homeassistant.auth.const import MFA_SESSION_EXPIRATION
+from homeassistant.core import callback
from homeassistant.util import dt as dt_util
from tests.common import (
MockUser, ensure_auth_manager_loaded, flush_store, CLIENT_ID)
@@ -138,6 +139,14 @@ async def test_auth_manager_from_config_auth_modules(mock_hass):
async def test_create_new_user(hass):
"""Test creating new user."""
+ events = []
+
+ @callback
+ def user_added(event):
+ events.append(event)
+
+ hass.bus.async_listen('user_added', user_added)
+
manager = await auth.auth_manager_from_config(hass, [{
'type': 'insecure_example',
'users': [{
@@ -160,6 +169,10 @@ async def test_create_new_user(hass):
assert user.is_owner is False
assert user.name == 'Test Name'
+ await hass.async_block_till_done()
+ assert len(events) == 1
+ assert events[0].data['user_id'] == user.id
+
async def test_login_as_existing_user(mock_hass):
"""Test login as existing user."""
@@ -289,6 +302,7 @@ async def test_saving_loading(hass, hass_storage):
store2 = auth_store.AuthStore(hass)
users = await store2.async_get_users()
assert len(users) == 1
+ assert users[0].permissions == user.permissions
assert users[0] == user
assert len(users[0].refresh_tokens) == 2
for r_token in users[0].refresh_tokens.values():
@@ -331,6 +345,14 @@ async def test_cannot_retrieve_expired_access_token(hass):
async def test_generating_system_user(hass):
"""Test that we can add a system user."""
+ events = []
+
+ @callback
+ def user_added(event):
+ events.append(event)
+
+ hass.bus.async_listen('user_added', user_added)
+
manager = await auth.auth_manager_from_config(hass, [], [])
user = await manager.async_create_system_user('Hass.io')
token = await manager.async_create_refresh_token(user)
@@ -338,6 +360,10 @@ async def test_generating_system_user(hass):
assert token is not None
assert token.client_id is None
+ await hass.async_block_till_done()
+ assert len(events) == 1
+ assert events[0].data['user_id'] == user.id
+
async def test_refresh_token_requires_client_for_user(hass):
"""Test create refresh token for a user with client_id."""
@@ -797,3 +823,50 @@ async def test_enable_mfa_for_user(hass, hass_storage):
# disable mfa for user don't enabled just silent fail
await manager.async_disable_user_mfa(user, 'insecure_example')
+
+
+async def test_async_remove_user(hass):
+ """Test removing a user."""
+ events = []
+
+ @callback
+ def user_removed(event):
+ events.append(event)
+
+ hass.bus.async_listen('user_removed', user_removed)
+
+ manager = await auth.auth_manager_from_config(hass, [{
+ 'type': 'insecure_example',
+ 'users': [{
+ 'username': 'test-user',
+ 'password': 'test-pass',
+ 'name': 'Test Name'
+ }]
+ }], [])
+ hass.auth = manager
+ ensure_auth_manager_loaded(manager)
+
+ # Add fake user with credentials for example auth provider.
+ user = MockUser(
+ id='mock-user',
+ is_owner=False,
+ is_active=False,
+ name='Paulus',
+ ).add_to_auth_manager(manager)
+ user.credentials.append(auth_models.Credentials(
+ id='mock-id',
+ auth_provider_type='insecure_example',
+ auth_provider_id=None,
+ data={'username': 'test-user'},
+ is_new=False,
+ ))
+ assert len(user.credentials) == 1
+
+ await hass.auth.async_remove_user(user)
+
+ assert len(await manager.async_get_users()) == 0
+ assert len(user.credentials) == 0
+
+ await hass.async_block_till_done()
+ assert len(events) == 1
+ assert events[0].data['user_id'] == user.id
diff --git a/tests/auth/test_models.py b/tests/auth/test_models.py
new file mode 100644
index 00000000000..c84bdc7390b
--- /dev/null
+++ b/tests/auth/test_models.py
@@ -0,0 +1,34 @@
+"""Tests for the auth models."""
+from homeassistant.auth import models, permissions
+
+
+def test_owner_fetching_owner_permissions():
+ """Test we fetch the owner permissions for an owner user."""
+ group = models.Group(name="Test Group", policy={})
+ owner = models.User(name="Test User", groups=[group], is_owner=True)
+ assert owner.permissions is permissions.OwnerPermissions
+
+
+def test_permissions_merged():
+ """Test we merge the groups permissions."""
+ group = models.Group(name="Test Group", policy={
+ 'entities': {
+ 'domains': {
+ 'switch': True
+ }
+ }
+ })
+ group2 = models.Group(name="Test Group", policy={
+ 'entities': {
+ 'entity_ids': {
+ 'light.kitchen': True
+ }
+ }
+ })
+ user = models.User(name="Test User", groups=[group, group2])
+ # Make sure we cache instance
+ assert user.permissions is user.permissions
+
+ assert user.permissions.check_entity('switch.bla') is True
+ assert user.permissions.check_entity('light.kitchen') is True
+ assert user.permissions.check_entity('light.not_kitchen') is False
diff --git a/tests/auth/test_permissions.py b/tests/auth/test_permissions.py
new file mode 100644
index 00000000000..71582dc281d
--- /dev/null
+++ b/tests/auth/test_permissions.py
@@ -0,0 +1,198 @@
+"""Tests for the auth permission system."""
+import pytest
+import voluptuous as vol
+
+from homeassistant.core import State
+from homeassistant.auth import permissions
+
+
+def test_entities_none():
+ """Test entity ID policy."""
+ policy = None
+ compiled = permissions._compile_entities(policy)
+ assert compiled('light.kitchen', []) is False
+
+
+def test_entities_empty():
+ """Test entity ID policy."""
+ policy = {}
+ permissions.ENTITY_POLICY_SCHEMA(policy)
+ compiled = permissions._compile_entities(policy)
+ assert compiled('light.kitchen', []) is False
+
+
+def test_entities_false():
+ """Test entity ID policy."""
+ policy = False
+ with pytest.raises(vol.Invalid):
+ permissions.ENTITY_POLICY_SCHEMA(policy)
+
+
+def test_entities_true():
+ """Test entity ID policy."""
+ policy = True
+ permissions.ENTITY_POLICY_SCHEMA(policy)
+ compiled = permissions._compile_entities(policy)
+ assert compiled('light.kitchen', []) is True
+
+
+def test_entities_domains_true():
+ """Test entity ID policy."""
+ policy = {
+ 'domains': True
+ }
+ permissions.ENTITY_POLICY_SCHEMA(policy)
+ compiled = permissions._compile_entities(policy)
+ assert compiled('light.kitchen', []) is True
+
+
+def test_entities_domains_domain_true():
+ """Test entity ID policy."""
+ policy = {
+ 'domains': {
+ 'light': True
+ }
+ }
+ permissions.ENTITY_POLICY_SCHEMA(policy)
+ compiled = permissions._compile_entities(policy)
+ assert compiled('light.kitchen', []) is True
+ assert compiled('switch.kitchen', []) is False
+
+
+def test_entities_domains_domain_false():
+ """Test entity ID policy."""
+ policy = {
+ 'domains': {
+ 'light': False
+ }
+ }
+ with pytest.raises(vol.Invalid):
+ permissions.ENTITY_POLICY_SCHEMA(policy)
+
+
+def test_entities_entity_ids_true():
+ """Test entity ID policy."""
+ policy = {
+ 'entity_ids': True
+ }
+ permissions.ENTITY_POLICY_SCHEMA(policy)
+ compiled = permissions._compile_entities(policy)
+ assert compiled('light.kitchen', []) is True
+
+
+def test_entities_entity_ids_false():
+ """Test entity ID policy."""
+ policy = {
+ 'entity_ids': False
+ }
+ with pytest.raises(vol.Invalid):
+ permissions.ENTITY_POLICY_SCHEMA(policy)
+
+
+def test_entities_entity_ids_entity_id_true():
+ """Test entity ID policy."""
+ policy = {
+ 'entity_ids': {
+ 'light.kitchen': True
+ }
+ }
+ permissions.ENTITY_POLICY_SCHEMA(policy)
+ compiled = permissions._compile_entities(policy)
+ assert compiled('light.kitchen', []) is True
+ assert compiled('switch.kitchen', []) is False
+
+
+def test_entities_entity_ids_entity_id_false():
+ """Test entity ID policy."""
+ policy = {
+ 'entity_ids': {
+ 'light.kitchen': False
+ }
+ }
+ with pytest.raises(vol.Invalid):
+ permissions.ENTITY_POLICY_SCHEMA(policy)
+
+
+def test_policy_perm_filter_states():
+ """Test filtering entitites."""
+ states = [
+ State('light.kitchen', 'on'),
+ State('light.living_room', 'off'),
+ State('light.balcony', 'on'),
+ ]
+ perm = permissions.PolicyPermissions({
+ 'entities': {
+ 'entity_ids': {
+ 'light.kitchen': True,
+ 'light.balcony': True,
+ }
+ }
+ })
+ filtered = perm.filter_states(states)
+ assert len(filtered) == 2
+ assert filtered == [states[0], states[2]]
+
+
+def test_owner_permissions():
+ """Test owner permissions access all."""
+ assert permissions.OwnerPermissions.check_entity('light.kitchen')
+ states = [
+ State('light.kitchen', 'on'),
+ State('light.living_room', 'off'),
+ State('light.balcony', 'on'),
+ ]
+ assert permissions.OwnerPermissions.filter_states(states) == states
+
+
+def test_default_policy_allow_all():
+ """Test that the default policy is to allow all entity actions."""
+ perm = permissions.PolicyPermissions(permissions.DEFAULT_POLICY)
+ assert perm.check_entity('light.kitchen')
+ states = [
+ State('light.kitchen', 'on'),
+ State('light.living_room', 'off'),
+ State('light.balcony', 'on'),
+ ]
+ assert perm.filter_states(states) == states
+
+
+def test_merging_permissions_true_rules_dict():
+ """Test merging policy with two entities."""
+ policy1 = {
+ 'something_else': True,
+ 'entities': {
+ 'entity_ids': {
+ 'light.kitchen': True,
+ }
+ }
+ }
+ policy2 = {
+ 'entities': {
+ 'entity_ids': True
+ }
+ }
+ assert permissions.merge_policies([policy1, policy2]) == {
+ 'something_else': True,
+ 'entities': {
+ 'entity_ids': True
+ }
+ }
+
+
+def test_merging_permissions_multiple_subcategories():
+ """Test merging policy with two entities."""
+ policy1 = {
+ 'entities': None
+ }
+ policy2 = {
+ 'entities': {
+ 'entity_ids': True,
+ }
+ }
+ policy3 = {
+ 'entities': True
+ }
+ assert permissions.merge_policies([policy1, policy2]) == policy2
+ assert permissions.merge_policies([policy1, policy3]) == policy3
+
+ assert permissions.merge_policies([policy2, policy3]) == policy3
diff --git a/tests/common.py b/tests/common.py
index ee181cfa2e9..44f934e4cb3 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -251,7 +251,7 @@ fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message)
@ha.callback
def async_fire_time_changed(hass, time):
"""Fire a time changes event."""
- hass.bus.async_fire(EVENT_TIME_CHANGED, {'now': time})
+ hass.bus.async_fire(EVENT_TIME_CHANGED, {'now': date_util.as_utc(time)})
fire_time_changed = threadsafe_callback_factory(async_fire_time_changed)
@@ -345,17 +345,44 @@ def mock_device_registry(hass, mock_entries=None):
return registry
+class MockGroup(auth_models.Group):
+ """Mock a group in Home Assistant."""
+
+ def __init__(self, id=None, name='Mock Group',
+ policy=auth_store.DEFAULT_POLICY):
+ """Mock a group."""
+ kwargs = {
+ 'name': name,
+ 'policy': policy,
+ }
+ if id is not None:
+ kwargs['id'] = id
+
+ super().__init__(**kwargs)
+
+ def add_to_hass(self, hass):
+ """Test helper to add entry to hass."""
+ return self.add_to_auth_manager(hass.auth)
+
+ def add_to_auth_manager(self, auth_mgr):
+ """Test helper to add entry to hass."""
+ ensure_auth_manager_loaded(auth_mgr)
+ auth_mgr._store._groups[self.id] = self
+ return self
+
+
class MockUser(auth_models.User):
"""Mock a user in Home Assistant."""
def __init__(self, id=None, is_owner=False, is_active=True,
- name='Mock User', system_generated=False):
+ name='Mock User', system_generated=False, groups=None):
"""Initialize mock user."""
kwargs = {
'is_owner': is_owner,
'is_active': is_active,
'name': name,
'system_generated': system_generated,
+ 'groups': groups or [],
}
if id is not None:
kwargs['id'] = id
diff --git a/tests/components/automation/test_geo_location.py b/tests/components/automation/test_geo_location.py
new file mode 100644
index 00000000000..130cdeef99c
--- /dev/null
+++ b/tests/components/automation/test_geo_location.py
@@ -0,0 +1,271 @@
+"""The tests for the geo location trigger."""
+import unittest
+
+from homeassistant.components import automation, zone
+from homeassistant.core import callback, Context
+from homeassistant.setup import setup_component
+
+from tests.common import get_test_home_assistant, mock_component
+from tests.components.automation import common
+
+
+class TestAutomationGeoLocation(unittest.TestCase):
+ """Test the geo location trigger."""
+
+ def setUp(self):
+ """Set up things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ mock_component(self.hass, 'group')
+ assert setup_component(self.hass, zone.DOMAIN, {
+ 'zone': {
+ 'name': 'test',
+ 'latitude': 32.880837,
+ 'longitude': -117.237561,
+ 'radius': 250,
+ }
+ })
+
+ self.calls = []
+
+ @callback
+ def record_call(service):
+ """Record calls."""
+ self.calls.append(service)
+
+ self.hass.services.register('test', 'automation', record_call)
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ def test_if_fires_on_zone_enter(self):
+ """Test for firing on zone enter."""
+ context = Context()
+ self.hass.states.set('geo_location.entity', 'hello', {
+ 'latitude': 32.881011,
+ 'longitude': -117.234758,
+ 'source': 'test_source'
+ })
+ self.hass.block_till_done()
+
+ assert setup_component(self.hass, automation.DOMAIN, {
+ automation.DOMAIN: {
+ 'trigger': {
+ 'platform': 'geo_location',
+ 'source': 'test_source',
+ 'zone': 'zone.test',
+ 'event': 'enter',
+ },
+ 'action': {
+ 'service': 'test.automation',
+ 'data_template': {
+ 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join((
+ 'platform', 'entity_id',
+ 'from_state.state', 'to_state.state',
+ 'zone.name'))
+ },
+
+ }
+ }
+ })
+
+ self.hass.states.set('geo_location.entity', 'hello', {
+ 'latitude': 32.880586,
+ 'longitude': -117.237564
+ }, context=context)
+ self.hass.block_till_done()
+
+ self.assertEqual(1, len(self.calls))
+ assert self.calls[0].context is context
+ self.assertEqual(
+ 'geo_location - geo_location.entity - hello - hello - test',
+ self.calls[0].data['some'])
+
+ # Set out of zone again so we can trigger call
+ self.hass.states.set('geo_location.entity', 'hello', {
+ 'latitude': 32.881011,
+ 'longitude': -117.234758
+ })
+ self.hass.block_till_done()
+
+ common.turn_off(self.hass)
+ self.hass.block_till_done()
+
+ self.hass.states.set('geo_location.entity', 'hello', {
+ 'latitude': 32.880586,
+ 'longitude': -117.237564
+ })
+ self.hass.block_till_done()
+
+ self.assertEqual(1, len(self.calls))
+
+ def test_if_not_fires_for_enter_on_zone_leave(self):
+ """Test for not firing on zone leave."""
+ self.hass.states.set('geo_location.entity', 'hello', {
+ 'latitude': 32.880586,
+ 'longitude': -117.237564,
+ 'source': 'test_source'
+ })
+ self.hass.block_till_done()
+
+ assert setup_component(self.hass, automation.DOMAIN, {
+ automation.DOMAIN: {
+ 'trigger': {
+ 'platform': 'geo_location',
+ 'source': 'test_source',
+ 'zone': 'zone.test',
+ 'event': 'enter',
+ },
+ 'action': {
+ 'service': 'test.automation',
+ }
+ }
+ })
+
+ self.hass.states.set('geo_location.entity', 'hello', {
+ 'latitude': 32.881011,
+ 'longitude': -117.234758
+ })
+ self.hass.block_till_done()
+
+ self.assertEqual(0, len(self.calls))
+
+ def test_if_fires_on_zone_leave(self):
+ """Test for firing on zone leave."""
+ self.hass.states.set('geo_location.entity', 'hello', {
+ 'latitude': 32.880586,
+ 'longitude': -117.237564,
+ 'source': 'test_source'
+ })
+ self.hass.block_till_done()
+
+ assert setup_component(self.hass, automation.DOMAIN, {
+ automation.DOMAIN: {
+ 'trigger': {
+ 'platform': 'geo_location',
+ 'source': 'test_source',
+ 'zone': 'zone.test',
+ 'event': 'leave',
+ },
+ 'action': {
+ 'service': 'test.automation',
+ }
+ }
+ })
+
+ self.hass.states.set('geo_location.entity', 'hello', {
+ 'latitude': 32.881011,
+ 'longitude': -117.234758,
+ 'source': 'test_source'
+ })
+ self.hass.block_till_done()
+
+ self.assertEqual(1, len(self.calls))
+
+ def test_if_not_fires_for_leave_on_zone_enter(self):
+ """Test for not firing on zone enter."""
+ self.hass.states.set('geo_location.entity', 'hello', {
+ 'latitude': 32.881011,
+ 'longitude': -117.234758,
+ 'source': 'test_source'
+ })
+ self.hass.block_till_done()
+
+ assert setup_component(self.hass, automation.DOMAIN, {
+ automation.DOMAIN: {
+ 'trigger': {
+ 'platform': 'geo_location',
+ 'source': 'test_source',
+ 'zone': 'zone.test',
+ 'event': 'leave',
+ },
+ 'action': {
+ 'service': 'test.automation',
+ }
+ }
+ })
+
+ self.hass.states.set('geo_location.entity', 'hello', {
+ 'latitude': 32.880586,
+ 'longitude': -117.237564
+ })
+ self.hass.block_till_done()
+
+ self.assertEqual(0, len(self.calls))
+
+ def test_if_fires_on_zone_appear(self):
+ """Test for firing if entity appears in zone."""
+ assert setup_component(self.hass, automation.DOMAIN, {
+ automation.DOMAIN: {
+ 'trigger': {
+ 'platform': 'geo_location',
+ 'source': 'test_source',
+ 'zone': 'zone.test',
+ 'event': 'enter',
+ },
+ 'action': {
+ 'service': 'test.automation',
+ 'data_template': {
+ 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join((
+ 'platform', 'entity_id',
+ 'from_state.state', 'to_state.state',
+ 'zone.name'))
+ },
+
+ }
+ }
+ })
+
+ # Entity appears in zone without previously existing outside the zone.
+ context = Context()
+ self.hass.states.set('geo_location.entity', 'hello', {
+ 'latitude': 32.880586,
+ 'longitude': -117.237564,
+ 'source': 'test_source'
+ }, context=context)
+ self.hass.block_till_done()
+
+ self.assertEqual(1, len(self.calls))
+ assert self.calls[0].context is context
+ self.assertEqual(
+ 'geo_location - geo_location.entity - - hello - test',
+ self.calls[0].data['some'])
+
+ def test_if_fires_on_zone_disappear(self):
+ """Test for firing if entity disappears from zone."""
+ self.hass.states.set('geo_location.entity', 'hello', {
+ 'latitude': 32.880586,
+ 'longitude': -117.237564,
+ 'source': 'test_source'
+ })
+ self.hass.block_till_done()
+
+ assert setup_component(self.hass, automation.DOMAIN, {
+ automation.DOMAIN: {
+ 'trigger': {
+ 'platform': 'geo_location',
+ 'source': 'test_source',
+ 'zone': 'zone.test',
+ 'event': 'leave',
+ },
+ 'action': {
+ 'service': 'test.automation',
+ 'data_template': {
+ 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join((
+ 'platform', 'entity_id',
+ 'from_state.state', 'to_state.state',
+ 'zone.name'))
+ },
+
+ }
+ }
+ })
+
+ # Entity disappears from zone without new coordinates outside the zone.
+ self.hass.states.async_remove('geo_location.entity')
+ self.hass.block_till_done()
+
+ self.assertEqual(1, len(self.calls))
+ self.assertEqual(
+ 'geo_location - geo_location.entity - hello - - test',
+ self.calls[0].data['some'])
diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py
index 84619ce4ee6..8e9e7dcf301 100644
--- a/tests/components/binary_sensor/test_mqtt.py
+++ b/tests/components/binary_sensor/test_mqtt.py
@@ -1,5 +1,8 @@
"""The tests for the MQTT binary sensor platform."""
+import json
import unittest
+from unittest.mock import Mock
+from datetime import timedelta
import homeassistant.core as ha
from homeassistant.setup import setup_component, async_setup_component
@@ -9,10 +12,12 @@ from homeassistant.components.mqtt.discovery import async_start
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE
+import homeassistant.util.dt as dt_util
+
from tests.common import (
get_test_home_assistant, fire_mqtt_message, async_fire_mqtt_message,
- mock_component, mock_mqtt_component, async_mock_mqtt_component,
- MockConfigEntry)
+ fire_time_changed, mock_component, mock_mqtt_component,
+ async_mock_mqtt_component, MockConfigEntry)
class TestSensorMQTT(unittest.TestCase):
@@ -21,6 +26,7 @@ class TestSensorMQTT(unittest.TestCase):
def setUp(self): # pylint: disable=invalid-name
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
+ self.hass.config_entries._async_schedule_save = Mock()
mock_mqtt_component(self.hass)
def tearDown(self): # pylint: disable=invalid-name
@@ -208,6 +214,48 @@ class TestSensorMQTT(unittest.TestCase):
self.hass.block_till_done()
self.assertEqual(2, len(events))
+ def test_off_delay(self):
+ """Test off_delay option."""
+ mock_component(self.hass, 'mqtt')
+ assert setup_component(self.hass, binary_sensor.DOMAIN, {
+ binary_sensor.DOMAIN: {
+ 'platform': 'mqtt',
+ 'name': 'test',
+ 'state_topic': 'test-topic',
+ 'payload_on': 'ON',
+ 'payload_off': 'OFF',
+ 'off_delay': 30,
+ 'force_update': True
+ }
+ })
+
+ events = []
+
+ @ha.callback
+ def callback(event):
+ """Verify event got called."""
+ events.append(event)
+
+ self.hass.bus.listen(EVENT_STATE_CHANGED, callback)
+
+ fire_mqtt_message(self.hass, 'test-topic', 'ON')
+ self.hass.block_till_done()
+ state = self.hass.states.get('binary_sensor.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertEqual(1, len(events))
+
+ fire_mqtt_message(self.hass, 'test-topic', 'ON')
+ self.hass.block_till_done()
+ state = self.hass.states.get('binary_sensor.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertEqual(2, len(events))
+
+ fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=30))
+ self.hass.block_till_done()
+ state = self.hass.states.get('binary_sensor.test')
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertEqual(3, len(events))
+
async def test_unique_id(hass):
"""Test unique id option only creates one sensor per unique_id."""
@@ -250,3 +298,41 @@ async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog):
await hass.async_block_till_done()
state = hass.states.get('binary_sensor.beer')
assert state is None
+
+
+async def test_entity_device_info_with_identifier(hass, mqtt_mock):
+ """Test MQTT binary sensor device registry integration."""
+ entry = MockConfigEntry(domain=mqtt.DOMAIN)
+ entry.add_to_hass(hass)
+ await async_start(hass, 'homeassistant', {}, entry)
+ registry = await hass.helpers.device_registry.async_get_registry()
+
+ data = json.dumps({
+ 'platform': 'mqtt',
+ 'name': 'Test 1',
+ 'state_topic': 'test-topic',
+ 'device': {
+ 'identifiers': ['helloworld'],
+ 'connections': [
+ ["mac", "02:5b:26:a8:dc:12"],
+ ],
+ 'manufacturer': 'Whatever',
+ 'name': 'Beer',
+ 'model': 'Glass',
+ 'sw_version': '0.1-beta',
+ },
+ 'unique_id': 'veryunique'
+ })
+ async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config',
+ data)
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({('mqtt', 'helloworld')}, set())
+ assert device is not None
+ assert device.identifiers == {('mqtt', 'helloworld')}
+ assert device.connections == {('mac', "02:5b:26:a8:dc:12")}
+ assert device.manufacturer == 'Whatever'
+ assert device.name == 'Beer'
+ assert device.model == 'Glass'
+ assert device.sw_version == '0.1-beta'
diff --git a/tests/components/binary_sensor/test_rflink.py b/tests/components/binary_sensor/test_rflink.py
new file mode 100644
index 00000000000..94f4208d5b8
--- /dev/null
+++ b/tests/components/binary_sensor/test_rflink.py
@@ -0,0 +1,178 @@
+"""
+Test for RFlink sensor components.
+
+Test setup of rflink sensor component/platform. Verify manual and
+automatic sensor creation.
+"""
+from datetime import timedelta
+from unittest.mock import patch
+
+from ..test_rflink import mock_rflink
+from homeassistant.components.rflink import (
+ CONF_RECONNECT_INTERVAL)
+
+import homeassistant.core as ha
+from homeassistant.const import (
+ EVENT_STATE_CHANGED, STATE_ON, STATE_OFF, STATE_UNAVAILABLE)
+import homeassistant.util.dt as dt_util
+
+from tests.common import async_fire_time_changed
+
+DOMAIN = 'binary_sensor'
+
+CONFIG = {
+ 'rflink': {
+ 'port': '/dev/ttyABC0',
+ 'ignore_devices': ['ignore_wildcard_*', 'ignore_sensor'],
+ },
+ DOMAIN: {
+ 'platform': 'rflink',
+ 'devices': {
+ 'test': {
+ 'name': 'test',
+ 'device_class': 'door',
+ },
+ 'test2': {
+ 'name': 'test2',
+ 'device_class': 'motion',
+ 'off_delay': 30,
+ 'force_update': True,
+ },
+ },
+ },
+}
+
+
+async def test_default_setup(hass, monkeypatch):
+ """Test all basic functionality of the rflink sensor component."""
+ # setup mocking rflink module
+ event_callback, create, _, disconnect_callback = await mock_rflink(
+ hass, CONFIG, DOMAIN, monkeypatch)
+
+ # make sure arguments are passed
+ assert create.call_args_list[0][1]['ignore']
+
+ # test default state of sensor loaded from config
+ config_sensor = hass.states.get('binary_sensor.test')
+ assert config_sensor
+ assert config_sensor.state == STATE_OFF
+ assert config_sensor.attributes['device_class'] == 'door'
+
+ # test event for config sensor
+ event_callback({
+ 'id': 'test',
+ 'command': 'on',
+ })
+ await hass.async_block_till_done()
+
+ assert hass.states.get('binary_sensor.test').state == STATE_ON
+
+ # test event for config sensor
+ event_callback({
+ 'id': 'test',
+ 'command': 'off',
+ })
+ await hass.async_block_till_done()
+
+ assert hass.states.get('binary_sensor.test').state == STATE_OFF
+
+
+async def test_entity_availability(hass, monkeypatch):
+ """If Rflink device is disconnected, entities should become unavailable."""
+ # Make sure Rflink mock does not 'recover' to quickly from the
+ # disconnect or else the unavailability cannot be measured
+ config = CONFIG
+ failures = [True, True]
+ config[CONF_RECONNECT_INTERVAL] = 60
+
+ # Create platform and entities
+ event_callback, create, _, disconnect_callback = await mock_rflink(
+ hass, config, DOMAIN, monkeypatch, failures=failures)
+
+ # Entities are available by default
+ assert hass.states.get('binary_sensor.test').state == STATE_OFF
+
+ # Mock a disconnect of the Rflink device
+ disconnect_callback()
+
+ # Wait for dispatch events to propagate
+ await hass.async_block_till_done()
+
+ # Entity should be unavailable
+ assert hass.states.get('binary_sensor.test').state == STATE_UNAVAILABLE
+
+ # Reconnect the Rflink device
+ disconnect_callback()
+
+ # Wait for dispatch events to propagate
+ await hass.async_block_till_done()
+
+ # Entities should be available again
+ assert hass.states.get('binary_sensor.test').state == STATE_OFF
+
+
+async def test_off_delay(hass, monkeypatch):
+ """Test off_delay option."""
+ # setup mocking rflink module
+ event_callback, create, _, disconnect_callback = await mock_rflink(
+ hass, CONFIG, DOMAIN, monkeypatch)
+
+ # make sure arguments are passed
+ assert create.call_args_list[0][1]['ignore']
+
+ events = []
+
+ on_event = {
+ 'id': 'test2',
+ 'command': 'on',
+ }
+
+ @ha.callback
+ def callback(event):
+ """Verify event got called."""
+ events.append(event)
+
+ hass.bus.async_listen(EVENT_STATE_CHANGED, callback)
+
+ now = dt_util.utcnow()
+ # fake time and turn on sensor
+ future = now + timedelta(seconds=0)
+ with patch(('homeassistant.helpers.event.'
+ 'dt_util.utcnow'), return_value=future):
+ async_fire_time_changed(hass, future)
+ event_callback(on_event)
+ await hass.async_block_till_done()
+ state = hass.states.get('binary_sensor.test2')
+ assert state.state == STATE_ON
+ assert len(events) == 1
+
+ # fake time and turn on sensor again
+ future = now + timedelta(seconds=15)
+ with patch(('homeassistant.helpers.event.'
+ 'dt_util.utcnow'), return_value=future):
+ async_fire_time_changed(hass, future)
+ event_callback(on_event)
+ await hass.async_block_till_done()
+ state = hass.states.get('binary_sensor.test2')
+ assert state.state == STATE_ON
+ assert len(events) == 2
+
+ # fake time and verify sensor still on (de-bounce)
+ future = now + timedelta(seconds=35)
+ with patch(('homeassistant.helpers.event.'
+ 'dt_util.utcnow'), return_value=future):
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+ state = hass.states.get('binary_sensor.test2')
+ assert state.state == STATE_ON
+ assert len(events) == 2
+
+ # fake time and verify sensor is off
+ future = now + timedelta(seconds=45)
+ with patch(('homeassistant.helpers.event.'
+ 'dt_util.utcnow'), return_value=future):
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+ state = hass.states.get('binary_sensor.test2')
+ assert state.state == STATE_OFF
+ assert len(events) == 3
diff --git a/tests/components/climate/test_dyson.py b/tests/components/climate/test_dyson.py
new file mode 100644
index 00000000000..6e8b63d64c4
--- /dev/null
+++ b/tests/components/climate/test_dyson.py
@@ -0,0 +1,358 @@
+"""Test the Dyson fan component."""
+import unittest
+from unittest import mock
+
+from libpurecoollink.const import (FocusMode, HeatMode, HeatState, HeatTarget,
+ TiltState)
+from libpurecoollink.dyson_pure_state import DysonPureHotCoolState
+from libpurecoollink.dyson_pure_hotcool_link import DysonPureHotCoolLink
+from homeassistant.components.climate import dyson
+from homeassistant.components import dyson as dyson_parent
+from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
+from homeassistant.setup import setup_component
+from tests.common import get_test_home_assistant
+
+
+class MockDysonState(DysonPureHotCoolState):
+ """Mock Dyson state."""
+
+ def __init__(self):
+ """Create new Mock Dyson State."""
+ pass
+
+
+def _get_device_with_no_state():
+ """Return a device with no state."""
+ device = mock.Mock(spec=DysonPureHotCoolLink)
+ device.name = "Device_name"
+ device.state = None
+ device.environmental_state = None
+ return device
+
+
+def _get_device_off():
+ """Return a device with state off."""
+ device = mock.Mock(spec=DysonPureHotCoolLink)
+ device.name = "Device_name"
+ device.state = mock.Mock()
+ device.environmental_state = mock.Mock()
+ return device
+
+
+def _get_device_focus():
+ """Return a device with fan state of focus mode."""
+ device = mock.Mock(spec=DysonPureHotCoolLink)
+ device.name = "Device_name"
+ device.state.focus_mode = FocusMode.FOCUS_ON.value
+ return device
+
+
+def _get_device_diffuse():
+ """Return a device with fan state of diffuse mode."""
+ device = mock.Mock(spec=DysonPureHotCoolLink)
+ device.name = "Device_name"
+ device.state.focus_mode = FocusMode.FOCUS_OFF.value
+ return device
+
+
+def _get_device_cool():
+ """Return a device with state of cooling."""
+ device = mock.Mock(spec=DysonPureHotCoolLink)
+ device.name = "Device_name"
+ device.state.tilt = TiltState.TILT_FALSE.value
+ device.state.focus_mode = FocusMode.FOCUS_OFF.value
+ device.state.heat_target = HeatTarget.celsius(12)
+ device.state.heat_mode = HeatMode.HEAT_OFF.value
+ device.state.heat_state = HeatState.HEAT_STATE_OFF.value
+ device.environmental_state.temperature = 288
+ device.environmental_state.humidity = 53
+ return device
+
+
+def _get_device_heat_off():
+ """Return a device with state of heat reached target."""
+ device = mock.Mock(spec=DysonPureHotCoolLink)
+ device.name = "Device_name"
+ device.state = mock.Mock()
+ device.state.tilt = TiltState.TILT_FALSE.value
+ device.state.focus_mode = FocusMode.FOCUS_ON.value
+ device.state.heat_target = HeatTarget.celsius(20)
+ device.state.heat_mode = HeatMode.HEAT_ON.value
+ device.state.heat_state = HeatState.HEAT_STATE_OFF.value
+ device.environmental_state.temperature = 293
+ device.environmental_state.humidity = 53
+ return device
+
+
+def _get_device_heat_on():
+ """Return a device with state of heating."""
+ device = mock.Mock(spec=DysonPureHotCoolLink)
+ device.name = "Device_name"
+ device.state = mock.Mock()
+ device.state.tilt = TiltState.TILT_FALSE.value
+ device.state.focus_mode = FocusMode.FOCUS_ON.value
+ device.state.heat_target = HeatTarget.celsius(23)
+ device.state.heat_mode = HeatMode.HEAT_ON.value
+ device.state.heat_state = HeatState.HEAT_STATE_ON.value
+ device.environmental_state.temperature = 289
+ device.environmental_state.humidity = 53
+ return device
+
+
+class DysonTest(unittest.TestCase):
+ """Dyson Climate component test class."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Set up things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ @mock.patch('libpurecoollink.dyson.DysonAccount.devices',
+ return_value=[_get_device_heat_on(), _get_device_cool()])
+ @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True)
+ def test_setup_component_with_parent_discovery(self, mocked_login,
+ mocked_devices):
+ """Test setup_component using discovery."""
+ setup_component(self.hass, dyson_parent.DOMAIN, {
+ dyson_parent.DOMAIN: {
+ dyson_parent.CONF_USERNAME: "email",
+ dyson_parent.CONF_PASSWORD: "password",
+ dyson_parent.CONF_LANGUAGE: "US",
+ }
+ })
+ self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 2)
+ self.hass.block_till_done()
+ for m in mocked_devices.return_value:
+ assert m.add_message_listener.called
+
+ def test_setup_component_without_devices(self):
+ """Test setup component with no devices."""
+ self.hass.data[dyson.DYSON_DEVICES] = []
+ add_devices = mock.MagicMock()
+ dyson.setup_platform(self.hass, None, add_devices)
+ add_devices.assert_not_called()
+
+ def test_setup_component_with_devices(self):
+ """Test setup component with valid devices."""
+ devices = [
+ _get_device_with_no_state(),
+ _get_device_off(),
+ _get_device_heat_on()
+ ]
+ self.hass.data[dyson.DYSON_DEVICES] = devices
+ add_devices = mock.MagicMock()
+ dyson.setup_platform(self.hass, None, add_devices, discovery_info={})
+ self.assertTrue(add_devices.called)
+
+ def test_setup_component_with_invalid_devices(self):
+ """Test setup component with invalid devices."""
+ devices = [
+ None,
+ "foo_bar"
+ ]
+ self.hass.data[dyson.DYSON_DEVICES] = devices
+ add_devices = mock.MagicMock()
+ dyson.setup_platform(self.hass, None, add_devices, discovery_info={})
+ add_devices.assert_called_with([])
+
+ def test_setup_component(self):
+ """Test setup component with devices."""
+ device_fan = _get_device_heat_on()
+ device_non_fan = _get_device_off()
+
+ def _add_device(devices):
+ assert len(devices) == 1
+ assert devices[0].name == "Device_name"
+
+ self.hass.data[dyson.DYSON_DEVICES] = [device_fan, device_non_fan]
+ dyson.setup_platform(self.hass, None, _add_device)
+
+ def test_dyson_set_temperature(self):
+ """Test set climate temperature."""
+ device = _get_device_heat_on()
+ device.temp_unit = TEMP_CELSIUS
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertFalse(entity.should_poll)
+
+ # Without target temp.
+ kwargs = {}
+ entity.set_temperature(**kwargs)
+ set_config = device.set_configuration
+ set_config.assert_not_called()
+
+ kwargs = {ATTR_TEMPERATURE: 23}
+ entity.set_temperature(**kwargs)
+ set_config = device.set_configuration
+ set_config.assert_called_with(
+ heat_mode=HeatMode.HEAT_ON,
+ heat_target=HeatTarget.celsius(23))
+
+ # Should clip the target temperature between 1 and 37 inclusive.
+ kwargs = {ATTR_TEMPERATURE: 50}
+ entity.set_temperature(**kwargs)
+ set_config = device.set_configuration
+ set_config.assert_called_with(
+ heat_mode=HeatMode.HEAT_ON,
+ heat_target=HeatTarget.celsius(37))
+
+ kwargs = {ATTR_TEMPERATURE: -5}
+ entity.set_temperature(**kwargs)
+ set_config = device.set_configuration
+ set_config.assert_called_with(
+ heat_mode=HeatMode.HEAT_ON,
+ heat_target=HeatTarget.celsius(1))
+
+ def test_dyson_set_temperature_when_cooling_mode(self):
+ """Test set climate temperature when heating is off."""
+ device = _get_device_cool()
+ device.temp_unit = TEMP_CELSIUS
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ entity.schedule_update_ha_state = mock.Mock()
+
+ kwargs = {ATTR_TEMPERATURE: 23}
+ entity.set_temperature(**kwargs)
+ set_config = device.set_configuration
+ set_config.assert_called_with(
+ heat_mode=HeatMode.HEAT_ON,
+ heat_target=HeatTarget.celsius(23))
+
+ def test_dyson_set_fan_mode(self):
+ """Test set fan mode."""
+ device = _get_device_heat_on()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertFalse(entity.should_poll)
+
+ entity.set_fan_mode(dyson.STATE_FOCUS)
+ set_config = device.set_configuration
+ set_config.assert_called_with(focus_mode=FocusMode.FOCUS_ON)
+
+ entity.set_fan_mode(dyson.STATE_DIFFUSE)
+ set_config = device.set_configuration
+ set_config.assert_called_with(focus_mode=FocusMode.FOCUS_OFF)
+
+ def test_dyson_fan_list(self):
+ """Test get fan list."""
+ device = _get_device_heat_on()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertEqual(len(entity.fan_list), 2)
+ self.assertTrue(dyson.STATE_FOCUS in entity.fan_list)
+ self.assertTrue(dyson.STATE_DIFFUSE in entity.fan_list)
+
+ def test_dyson_fan_mode_focus(self):
+ """Test fan focus mode."""
+ device = _get_device_focus()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertEqual(entity.current_fan_mode, dyson.STATE_FOCUS)
+
+ def test_dyson_fan_mode_diffuse(self):
+ """Test fan diffuse mode."""
+ device = _get_device_diffuse()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertEqual(entity.current_fan_mode, dyson.STATE_DIFFUSE)
+
+ def test_dyson_set_operation_mode(self):
+ """Test set operation mode."""
+ device = _get_device_heat_on()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertFalse(entity.should_poll)
+
+ entity.set_operation_mode(dyson.STATE_HEAT)
+ set_config = device.set_configuration
+ set_config.assert_called_with(heat_mode=HeatMode.HEAT_ON)
+
+ entity.set_operation_mode(dyson.STATE_COOL)
+ set_config = device.set_configuration
+ set_config.assert_called_with(heat_mode=HeatMode.HEAT_OFF)
+
+ def test_dyson_operation_list(self):
+ """Test get operation list."""
+ device = _get_device_heat_on()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertEqual(len(entity.operation_list), 2)
+ self.assertTrue(dyson.STATE_HEAT in entity.operation_list)
+ self.assertTrue(dyson.STATE_COOL in entity.operation_list)
+
+ def test_dyson_heat_off(self):
+ """Test turn off heat."""
+ device = _get_device_heat_off()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ entity.set_operation_mode(dyson.STATE_COOL)
+ set_config = device.set_configuration
+ set_config.assert_called_with(heat_mode=HeatMode.HEAT_OFF)
+
+ def test_dyson_heat_on(self):
+ """Test turn on heat."""
+ device = _get_device_heat_on()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ entity.set_operation_mode(dyson.STATE_HEAT)
+ set_config = device.set_configuration
+ set_config.assert_called_with(heat_mode=HeatMode.HEAT_ON)
+
+ def test_dyson_heat_value_on(self):
+ """Test get heat value on."""
+ device = _get_device_heat_on()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertEqual(entity.current_operation, dyson.STATE_HEAT)
+
+ def test_dyson_heat_value_off(self):
+ """Test get heat value off."""
+ device = _get_device_cool()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertEqual(entity.current_operation, dyson.STATE_COOL)
+
+ def test_dyson_heat_value_idle(self):
+ """Test get heat value idle."""
+ device = _get_device_heat_off()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertEqual(entity.current_operation, dyson.STATE_IDLE)
+
+ def test_on_message(self):
+ """Test when message is received."""
+ device = _get_device_heat_on()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ entity.schedule_update_ha_state = mock.Mock()
+ entity.on_message(MockDysonState())
+ entity.schedule_update_ha_state.assert_called_with()
+
+ def test_general_properties(self):
+ """Test properties of entity."""
+ device = _get_device_with_no_state()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertEqual(entity.should_poll, False)
+ self.assertEqual(entity.supported_features, dyson.SUPPORT_FLAGS)
+ self.assertEqual(entity.temperature_unit, TEMP_CELSIUS)
+
+ def test_property_current_humidity(self):
+ """Test properties of current humidity."""
+ device = _get_device_heat_on()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertEqual(entity.current_humidity, 53)
+
+ def test_property_current_humidity_with_invalid_env_state(self):
+ """Test properties of current humidity with invalid env state."""
+ device = _get_device_off()
+ device.environmental_state.humidity = 0
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertEqual(entity.current_humidity, None)
+
+ def test_property_current_humidity_without_env_state(self):
+ """Test properties of current humidity without env state."""
+ device = _get_device_with_no_state()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertEqual(entity.current_humidity, None)
+
+ def test_property_current_temperature(self):
+ """Test properties of current temperature."""
+ device = _get_device_heat_on()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ # Result should be in celsius, hence then subtraction of 273.
+ self.assertEqual(entity.current_temperature, 289 - 273)
+
+ def test_property_target_temperature(self):
+ """Test properties of target temperature."""
+ device = _get_device_heat_on()
+ entity = dyson.DysonPureHotCoolLinkDevice(device)
+ self.assertEqual(entity.target_temperature, 23)
diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py
index c63dbf26690..16fe0a6639d 100644
--- a/tests/components/climate/test_mqtt.py
+++ b/tests/components/climate/test_mqtt.py
@@ -119,14 +119,14 @@ class TestMQTTClimate(unittest.TestCase):
assert setup_component(self.hass, climate.DOMAIN, config)
state = self.hass.states.get(ENTITY_CLIMATE)
- self.assertEqual("off", state.attributes.get('operation_mode'))
- self.assertEqual("off", state.state)
+ self.assertEqual(None, state.attributes.get('operation_mode'))
+ self.assertEqual("unknown", state.state)
common.set_operation_mode(self.hass, "cool", ENTITY_CLIMATE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
- self.assertEqual("off", state.attributes.get('operation_mode'))
- self.assertEqual("off", state.state)
+ self.assertEqual(None, state.attributes.get('operation_mode'))
+ self.assertEqual("unknown", state.state)
fire_mqtt_message(self.hass, 'mode-state', 'cool')
self.hass.block_till_done()
@@ -189,12 +189,12 @@ class TestMQTTClimate(unittest.TestCase):
assert setup_component(self.hass, climate.DOMAIN, config)
state = self.hass.states.get(ENTITY_CLIMATE)
- self.assertEqual("low", state.attributes.get('fan_mode'))
+ self.assertEqual(None, state.attributes.get('fan_mode'))
common.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
- self.assertEqual("low", state.attributes.get('fan_mode'))
+ self.assertEqual(None, state.attributes.get('fan_mode'))
fire_mqtt_message(self.hass, 'fan-state', 'high')
self.hass.block_till_done()
@@ -237,12 +237,12 @@ class TestMQTTClimate(unittest.TestCase):
assert setup_component(self.hass, climate.DOMAIN, config)
state = self.hass.states.get(ENTITY_CLIMATE)
- self.assertEqual("off", state.attributes.get('swing_mode'))
+ self.assertEqual(None, state.attributes.get('swing_mode'))
common.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
- self.assertEqual("off", state.attributes.get('swing_mode'))
+ self.assertEqual(None, state.attributes.get('swing_mode'))
fire_mqtt_message(self.hass, 'swing-state', 'on')
self.hass.block_till_done()
@@ -310,14 +310,14 @@ class TestMQTTClimate(unittest.TestCase):
assert setup_component(self.hass, climate.DOMAIN, config)
state = self.hass.states.get(ENTITY_CLIMATE)
- self.assertEqual(21, state.attributes.get('temperature'))
+ self.assertEqual(None, state.attributes.get('temperature'))
common.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE)
self.hass.block_till_done()
common.set_temperature(self.hass, temperature=47,
entity_id=ENTITY_CLIMATE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
- self.assertEqual(21, state.attributes.get('temperature'))
+ self.assertEqual(None, state.attributes.get('temperature'))
fire_mqtt_message(self.hass, 'temperature-state', '1701')
self.hass.block_till_done()
@@ -539,28 +539,28 @@ class TestMQTTClimate(unittest.TestCase):
# Operation Mode
state = self.hass.states.get(ENTITY_CLIMATE)
- self.assertEqual("off", state.attributes.get('operation_mode'))
+ self.assertEqual(None, state.attributes.get('operation_mode'))
fire_mqtt_message(self.hass, 'mode-state', '"cool"')
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual("cool", state.attributes.get('operation_mode'))
# Fan Mode
- self.assertEqual("low", state.attributes.get('fan_mode'))
+ self.assertEqual(None, state.attributes.get('fan_mode'))
fire_mqtt_message(self.hass, 'fan-state', '"high"')
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('high', state.attributes.get('fan_mode'))
# Swing Mode
- self.assertEqual("off", state.attributes.get('swing_mode'))
+ self.assertEqual(None, state.attributes.get('swing_mode'))
fire_mqtt_message(self.hass, 'swing-state', '"on"')
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual("on", state.attributes.get('swing_mode'))
# Temperature - with valid value
- self.assertEqual(21, state.attributes.get('temperature'))
+ self.assertEqual(None, state.attributes.get('temperature'))
fire_mqtt_message(self.hass, 'temperature-state', '"1031"')
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
@@ -653,6 +653,19 @@ class TestMQTTClimate(unittest.TestCase):
self.assertIsInstance(max_temp, float)
self.assertEqual(60, max_temp)
+ def test_temp_step_custom(self):
+ """Test a custom temp step."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config['climate']['temp_step'] = 0.01
+
+ assert setup_component(self.hass, climate.DOMAIN, config)
+
+ state = self.hass.states.get(ENTITY_CLIMATE)
+ temp_step = state.attributes.get('target_temp_step')
+
+ self.assertIsInstance(temp_step, float)
+ self.assertEqual(0.01, temp_step)
+
async def test_discovery_removal_climate(hass, mqtt_mock, caplog):
"""Test removal of discovered climate."""
diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py
index 5d4b356b9b2..e27760bd6ed 100644
--- a/tests/components/cloud/test_http_api.py
+++ b/tests/components/cloud/test_http_api.py
@@ -7,6 +7,7 @@ from jose import jwt
from homeassistant.components.cloud import (
DOMAIN, auth_api, iot, STORAGE_ENABLE_GOOGLE, STORAGE_ENABLE_ALEXA)
+from homeassistant.util import dt as dt_util
from tests.common import mock_coro
@@ -352,24 +353,89 @@ async def test_websocket_status_not_logged_in(hass, hass_ws_client):
}
-async def test_websocket_subscription(hass, hass_ws_client, aioclient_mock,
- mock_auth):
- """Test querying the status."""
- aioclient_mock.get(SUBSCRIPTION_INFO_URL, json={'return': 'value'})
+async def test_websocket_subscription_reconnect(
+ hass, hass_ws_client, aioclient_mock, mock_auth):
+ """Test querying the status and connecting because valid account."""
+ aioclient_mock.get(SUBSCRIPTION_INFO_URL, json={'provider': 'stripe'})
+ hass.data[DOMAIN].id_token = jwt.encode({
+ 'email': 'hello@home-assistant.io',
+ 'custom:sub-exp': dt_util.utcnow().date().isoformat()
+ }, 'test')
+ client = await hass_ws_client(hass)
+
+ with patch(
+ 'homeassistant.components.cloud.auth_api.renew_access_token'
+ ) as mock_renew, patch(
+ 'homeassistant.components.cloud.iot.CloudIoT.connect'
+ ) as mock_connect:
+ await client.send_json({
+ 'id': 5,
+ 'type': 'cloud/subscription'
+ })
+ response = await client.receive_json()
+
+ assert response['result'] == {
+ 'provider': 'stripe'
+ }
+ assert len(mock_renew.mock_calls) == 1
+ assert len(mock_connect.mock_calls) == 1
+
+
+async def test_websocket_subscription_no_reconnect_if_connected(
+ hass, hass_ws_client, aioclient_mock, mock_auth):
+ """Test querying the status and not reconnecting because still expired."""
+ aioclient_mock.get(SUBSCRIPTION_INFO_URL, json={'provider': 'stripe'})
+ hass.data[DOMAIN].iot.state = iot.STATE_CONNECTED
+ hass.data[DOMAIN].id_token = jwt.encode({
+ 'email': 'hello@home-assistant.io',
+ 'custom:sub-exp': dt_util.utcnow().date().isoformat()
+ }, 'test')
+ client = await hass_ws_client(hass)
+
+ with patch(
+ 'homeassistant.components.cloud.auth_api.renew_access_token'
+ ) as mock_renew, patch(
+ 'homeassistant.components.cloud.iot.CloudIoT.connect'
+ ) as mock_connect:
+ await client.send_json({
+ 'id': 5,
+ 'type': 'cloud/subscription'
+ })
+ response = await client.receive_json()
+
+ assert response['result'] == {
+ 'provider': 'stripe'
+ }
+ assert len(mock_renew.mock_calls) == 0
+ assert len(mock_connect.mock_calls) == 0
+
+
+async def test_websocket_subscription_no_reconnect_if_expired(
+ hass, hass_ws_client, aioclient_mock, mock_auth):
+ """Test querying the status and not reconnecting because still expired."""
+ aioclient_mock.get(SUBSCRIPTION_INFO_URL, json={'provider': 'stripe'})
hass.data[DOMAIN].id_token = jwt.encode({
'email': 'hello@home-assistant.io',
'custom:sub-exp': '2018-01-03'
}, 'test')
client = await hass_ws_client(hass)
- await client.send_json({
- 'id': 5,
- 'type': 'cloud/subscription'
- })
- response = await client.receive_json()
+
+ with patch(
+ 'homeassistant.components.cloud.auth_api.renew_access_token'
+ ) as mock_renew, patch(
+ 'homeassistant.components.cloud.iot.CloudIoT.connect'
+ ) as mock_connect:
+ await client.send_json({
+ 'id': 5,
+ 'type': 'cloud/subscription'
+ })
+ response = await client.receive_json()
assert response['result'] == {
- 'return': 'value'
+ 'provider': 'stripe'
}
+ assert len(mock_renew.mock_calls) == 1
+ assert len(mock_connect.mock_calls) == 1
async def test_websocket_subscription_fail(hass, hass_ws_client,
diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py
index 8695830eae9..61518f0f0e8 100644
--- a/tests/components/cloud/test_init.py
+++ b/tests/components/cloud/test_init.py
@@ -155,14 +155,14 @@ def test_subscription_expired(hass):
with patch.object(cl, '_decode_claims', return_value=token_val), \
patch('homeassistant.util.dt.utcnow',
return_value=utcnow().replace(
- year=2017, month=11, day=15, hour=23, minute=59,
+ year=2017, month=11, day=19, hour=23, minute=59,
second=59)):
assert not cl.subscription_expired
with patch.object(cl, '_decode_claims', return_value=token_val), \
patch('homeassistant.util.dt.utcnow',
return_value=utcnow().replace(
- year=2017, month=11, day=16, hour=0, minute=0,
+ year=2017, month=11, day=20, hour=0, minute=0,
second=0)):
assert cl.subscription_expired
diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py
index cd04eedf08e..f7e348e8476 100644
--- a/tests/components/config/test_auth.py
+++ b/tests/components/config/test_auth.py
@@ -6,7 +6,7 @@ import pytest
from homeassistant.auth import models as auth_models
from homeassistant.components.config import auth as auth_config
-from tests.common import MockUser, CLIENT_ID
+from tests.common import MockGroup, MockUser, CLIENT_ID
@pytest.fixture(autouse=True)
@@ -39,10 +39,13 @@ async def test_list_requires_owner(hass, hass_ws_client, hass_access_token):
async def test_list(hass, hass_ws_client):
"""Test get users."""
+ group = MockGroup().add_to_hass(hass)
+
owner = MockUser(
id='abc',
name='Test Owner',
is_owner=True,
+ groups=[group],
).add_to_hass(hass)
owner.credentials.append(auth_models.Credentials(
@@ -61,6 +64,7 @@ async def test_list(hass, hass_ws_client):
id='hij',
name='Inactive User',
is_active=False,
+ groups=[group],
).add_to_hass(hass)
refresh_token = await hass.auth.async_create_refresh_token(
@@ -83,6 +87,7 @@ async def test_list(hass, hass_ws_client):
'is_owner': True,
'is_active': True,
'system_generated': False,
+ 'group_ids': [group.id for group in owner.groups],
'credentials': [{'type': 'homeassistant'}]
}
assert data[1] == {
@@ -91,6 +96,7 @@ async def test_list(hass, hass_ws_client):
'is_owner': False,
'is_active': True,
'system_generated': True,
+ 'group_ids': [],
'credentials': [],
}
assert data[2] == {
@@ -99,6 +105,7 @@ async def test_list(hass, hass_ws_client):
'is_owner': False,
'is_active': False,
'system_generated': False,
+ 'group_ids': [group.id for group in inactive.groups],
'credentials': [],
}
diff --git a/tests/components/cover/test_deconz.py b/tests/components/cover/test_deconz.py
index 60de9cffdc1..e9c630823bd 100644
--- a/tests/components/cover/test_deconz.py
+++ b/tests/components/cover/test_deconz.py
@@ -13,7 +13,15 @@ SUPPORTED_COVERS = {
"id": "Cover 1 id",
"name": "Cover 1 name",
"type": "Level controllable output",
- "state": {}
+ "state": {},
+ "modelid": "Not zigbee spec"
+ },
+ "2": {
+ "id": "Cover 2 id",
+ "name": "Cover 2 name",
+ "type": "Window covering device",
+ "state": {},
+ "modelid": "lumi.curtain"
}
}
@@ -62,7 +70,7 @@ async def test_cover(hass):
await setup_bridge(hass, {"lights": SUPPORTED_COVERS})
assert "cover.cover_1_name" in hass.data[deconz.DATA_DECONZ_ID]
assert len(SUPPORTED_COVERS) == len(COVER_TYPES)
- assert len(hass.states.async_all()) == 2
+ assert len(hass.states.async_all()) == 3
async def test_add_new_cover(hass):
diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py
index 355f620520a..282b1d2873f 100644
--- a/tests/components/cover/test_mqtt.py
+++ b/tests/components/cover/test_mqtt.py
@@ -1,4 +1,5 @@
"""The tests for the MQTT cover platform."""
+import json
import unittest
from homeassistant.components import cover, mqtt
@@ -615,7 +616,7 @@ class TestCoverMQTT(unittest.TestCase):
'tilt-command-topic', 'tilt-status-topic', 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None,
False, None, 100, 0, 0, 100, False, False, None, None, None,
- None)
+ None, None)
self.assertEqual(44, mqtt_cover.find_percentage_in_range(44))
@@ -626,7 +627,7 @@ class TestCoverMQTT(unittest.TestCase):
'tilt-command-topic', 'tilt-status-topic', 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None,
False, None, 180, 80, 80, 180, False, False, None, None, None,
- None)
+ None, None)
self.assertEqual(40, mqtt_cover.find_percentage_in_range(120))
@@ -637,7 +638,7 @@ class TestCoverMQTT(unittest.TestCase):
'tilt-command-topic', 'tilt-status-topic', 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None,
False, None, 100, 0, 0, 100, False, True, None, None, None,
- None)
+ None, None)
self.assertEqual(56, mqtt_cover.find_percentage_in_range(44))
@@ -648,7 +649,7 @@ class TestCoverMQTT(unittest.TestCase):
'tilt-command-topic', 'tilt-status-topic', 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None,
False, None, 180, 80, 80, 180, False, True, None, None, None,
- None)
+ None, None)
self.assertEqual(60, mqtt_cover.find_percentage_in_range(120))
@@ -659,7 +660,7 @@ class TestCoverMQTT(unittest.TestCase):
'tilt-command-topic', 'tilt-status-topic', 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None,
False, None, 100, 0, 0, 100, False, False, None, None, None,
- None)
+ None, None)
self.assertEqual(44, mqtt_cover.find_in_range_from_percent(44))
@@ -670,7 +671,7 @@ class TestCoverMQTT(unittest.TestCase):
'tilt-command-topic', 'tilt-status-topic', 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None,
False, None, 180, 80, 80, 180, False, False, None, None, None,
- None)
+ None, None)
self.assertEqual(120, mqtt_cover.find_in_range_from_percent(40))
@@ -681,7 +682,7 @@ class TestCoverMQTT(unittest.TestCase):
'tilt-command-topic', 'tilt-status-topic', 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None,
False, None, 100, 0, 0, 100, False, True, None, None, None,
- None)
+ None, None)
self.assertEqual(44, mqtt_cover.find_in_range_from_percent(56))
@@ -692,7 +693,7 @@ class TestCoverMQTT(unittest.TestCase):
'tilt-command-topic', 'tilt-status-topic', 0, False,
'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None,
False, None, 180, 80, 80, 180, False, True, None, None, None,
- None)
+ None, None)
self.assertEqual(120, mqtt_cover.find_in_range_from_percent(60))
@@ -810,3 +811,42 @@ async def test_unique_id(hass):
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(cover.DOMAIN)) == 1
+
+
+async def test_entity_device_info_with_identifier(hass, mqtt_mock):
+ """Test MQTT cover device registry integration."""
+ entry = MockConfigEntry(domain=mqtt.DOMAIN)
+ entry.add_to_hass(hass)
+ await async_start(hass, 'homeassistant', {}, entry)
+ registry = await hass.helpers.device_registry.async_get_registry()
+
+ data = json.dumps({
+ 'platform': 'mqtt',
+ 'name': 'Test 1',
+ 'state_topic': 'test-topic',
+ 'command_topic': 'test-command-topic',
+ 'device': {
+ 'identifiers': ['helloworld'],
+ 'connections': [
+ ["mac", "02:5b:26:a8:dc:12"],
+ ],
+ 'manufacturer': 'Whatever',
+ 'name': 'Beer',
+ 'model': 'Glass',
+ 'sw_version': '0.1-beta',
+ },
+ 'unique_id': 'veryunique'
+ })
+ async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config',
+ data)
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({('mqtt', 'helloworld')}, set())
+ assert device is not None
+ assert device.identifiers == {('mqtt', 'helloworld')}
+ assert device.connections == {('mac', "02:5b:26:a8:dc:12")}
+ assert device.manufacturer == 'Whatever'
+ assert device.name == 'Beer'
+ assert device.model == 'Glass'
+ assert device.sw_version == '0.1-beta'
diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py
index 9b0a5cd9052..3de8e969140 100644
--- a/tests/components/emulated_hue/test_init.py
+++ b/tests/components/emulated_hue/test_init.py
@@ -1,7 +1,5 @@
"""Test the Emulated Hue component."""
-import json
-
-from unittest.mock import patch, Mock, mock_open, MagicMock
+from unittest.mock import patch, Mock, MagicMock
from homeassistant.components.emulated_hue import Config
@@ -14,30 +12,30 @@ def test_config_google_home_entity_id_to_number():
'type': 'google_home'
})
- mop = mock_open(read_data=json.dumps({'1': 'light.test2'}))
- handle = mop()
+ with patch('homeassistant.components.emulated_hue.load_json',
+ return_value={'1': 'light.test2'}) as json_loader:
+ with patch('homeassistant.components.emulated_hue'
+ '.save_json') as json_saver:
+ number = conf.entity_id_to_number('light.test')
+ assert number == '2'
- with patch('homeassistant.util.json.open', mop, create=True):
- with patch('homeassistant.util.json.os.open', return_value=0):
- with patch('homeassistant.util.json.os.replace'):
- number = conf.entity_id_to_number('light.test')
- assert number == '2'
- assert handle.write.call_count == 1
- assert json.loads(handle.write.mock_calls[0][1][0]) == {
- '1': 'light.test2',
- '2': 'light.test',
- }
+ assert json_saver.mock_calls[0][1][1] == {
+ '1': 'light.test2', '2': 'light.test'
+ }
- number = conf.entity_id_to_number('light.test')
- assert number == '2'
- assert handle.write.call_count == 1
+ assert json_saver.call_count == 1
+ assert json_loader.call_count == 1
- number = conf.entity_id_to_number('light.test2')
- assert number == '1'
- assert handle.write.call_count == 1
+ number = conf.entity_id_to_number('light.test')
+ assert number == '2'
+ assert json_saver.call_count == 1
- entity_id = conf.number_to_entity_id('1')
- assert entity_id == 'light.test2'
+ number = conf.entity_id_to_number('light.test2')
+ assert number == '1'
+ assert json_saver.call_count == 1
+
+ entity_id = conf.number_to_entity_id('1')
+ assert entity_id == 'light.test2'
def test_config_google_home_entity_id_to_number_altered():
@@ -48,30 +46,30 @@ def test_config_google_home_entity_id_to_number_altered():
'type': 'google_home'
})
- mop = mock_open(read_data=json.dumps({'21': 'light.test2'}))
- handle = mop()
+ with patch('homeassistant.components.emulated_hue.load_json',
+ return_value={'21': 'light.test2'}) as json_loader:
+ with patch('homeassistant.components.emulated_hue'
+ '.save_json') as json_saver:
+ number = conf.entity_id_to_number('light.test')
+ assert number == '22'
+ assert json_saver.call_count == 1
+ assert json_loader.call_count == 1
- with patch('homeassistant.util.json.open', mop, create=True):
- with patch('homeassistant.util.json.os.open', return_value=0):
- with patch('homeassistant.util.json.os.replace'):
- number = conf.entity_id_to_number('light.test')
- assert number == '22'
- assert handle.write.call_count == 1
- assert json.loads(handle.write.mock_calls[0][1][0]) == {
- '21': 'light.test2',
- '22': 'light.test',
- }
+ assert json_saver.mock_calls[0][1][1] == {
+ '21': 'light.test2',
+ '22': 'light.test',
+ }
- number = conf.entity_id_to_number('light.test')
- assert number == '22'
- assert handle.write.call_count == 1
+ number = conf.entity_id_to_number('light.test')
+ assert number == '22'
+ assert json_saver.call_count == 1
- number = conf.entity_id_to_number('light.test2')
- assert number == '21'
- assert handle.write.call_count == 1
+ number = conf.entity_id_to_number('light.test2')
+ assert number == '21'
+ assert json_saver.call_count == 1
- entity_id = conf.number_to_entity_id('21')
- assert entity_id == 'light.test2'
+ entity_id = conf.number_to_entity_id('21')
+ assert entity_id == 'light.test2'
def test_config_google_home_entity_id_to_number_empty():
@@ -82,29 +80,29 @@ def test_config_google_home_entity_id_to_number_empty():
'type': 'google_home'
})
- mop = mock_open(read_data='')
- handle = mop()
+ with patch('homeassistant.components.emulated_hue.load_json',
+ return_value={}) as json_loader:
+ with patch('homeassistant.components.emulated_hue'
+ '.save_json') as json_saver:
+ number = conf.entity_id_to_number('light.test')
+ assert number == '1'
+ assert json_saver.call_count == 1
+ assert json_loader.call_count == 1
- with patch('homeassistant.util.json.open', mop, create=True):
- with patch('homeassistant.util.json.os.open', return_value=0):
- with patch('homeassistant.util.json.os.replace'):
- number = conf.entity_id_to_number('light.test')
- assert number == '1'
- assert handle.write.call_count == 1
- assert json.loads(handle.write.mock_calls[0][1][0]) == {
- '1': 'light.test',
- }
+ assert json_saver.mock_calls[0][1][1] == {
+ '1': 'light.test',
+ }
- number = conf.entity_id_to_number('light.test')
- assert number == '1'
- assert handle.write.call_count == 1
+ number = conf.entity_id_to_number('light.test')
+ assert number == '1'
+ assert json_saver.call_count == 1
- number = conf.entity_id_to_number('light.test2')
- assert number == '2'
- assert handle.write.call_count == 2
+ number = conf.entity_id_to_number('light.test2')
+ assert number == '2'
+ assert json_saver.call_count == 2
- entity_id = conf.number_to_entity_id('2')
- assert entity_id == 'light.test2'
+ entity_id = conf.number_to_entity_id('2')
+ assert entity_id == 'light.test2'
def test_config_alexa_entity_id_to_number():
diff --git a/tests/components/fan/test_mqtt.py b/tests/components/fan/test_mqtt.py
index 7434e5aa1c9..e2742eeba7d 100644
--- a/tests/components/fan/test_mqtt.py
+++ b/tests/components/fan/test_mqtt.py
@@ -1,4 +1,5 @@
"""Test MQTT fans."""
+import json
import unittest
from homeassistant.setup import setup_component, async_setup_component
@@ -8,7 +9,7 @@ from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE
from tests.common import (
mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message,
- get_test_home_assistant, async_mock_mqtt_component)
+ get_test_home_assistant, async_mock_mqtt_component, MockConfigEntry)
class TestMqttFan(unittest.TestCase):
@@ -108,7 +109,8 @@ class TestMqttFan(unittest.TestCase):
async def test_discovery_removal_fan(hass, mqtt_mock, caplog):
"""Test removal of discovered fan."""
- await async_start(hass, 'homeassistant', {})
+ entry = MockConfigEntry(domain='mqtt')
+ await async_start(hass, 'homeassistant', {}, entry)
data = (
'{ "name": "Beer",'
' "command_topic": "test_topic" }'
@@ -150,3 +152,42 @@ async def test_unique_id(hass):
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(fan.DOMAIN)) == 1
+
+
+async def test_entity_device_info_with_identifier(hass, mqtt_mock):
+ """Test MQTT fan device registry integration."""
+ entry = MockConfigEntry(domain='mqtt')
+ entry.add_to_hass(hass)
+ await async_start(hass, 'homeassistant', {}, entry)
+ registry = await hass.helpers.device_registry.async_get_registry()
+
+ data = json.dumps({
+ 'platform': 'mqtt',
+ 'name': 'Test 1',
+ 'state_topic': 'test-topic',
+ 'command_topic': 'test-command-topic',
+ 'device': {
+ 'identifiers': ['helloworld'],
+ 'connections': [
+ ["mac", "02:5b:26:a8:dc:12"],
+ ],
+ 'manufacturer': 'Whatever',
+ 'name': 'Beer',
+ 'model': 'Glass',
+ 'sw_version': '0.1-beta',
+ },
+ 'unique_id': 'veryunique'
+ })
+ async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config',
+ data)
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({('mqtt', 'helloworld')}, set())
+ assert device is not None
+ assert device.identifiers == {('mqtt', 'helloworld')}
+ assert device.connections == {('mac', "02:5b:26:a8:dc:12")}
+ assert device.manufacturer == 'Whatever'
+ assert device.name == 'Beer'
+ assert device.model == 'Glass'
+ assert device.sw_version == '0.1-beta'
diff --git a/tests/components/geo_location/test_geo_json_events.py b/tests/components/geo_location/test_geo_json_events.py
index 5ce508289dd..00fc9f8c996 100644
--- a/tests/components/geo_location/test_geo_json_events.py
+++ b/tests/components/geo_location/test_geo_json_events.py
@@ -3,7 +3,9 @@ import unittest
from unittest import mock
from unittest.mock import patch, MagicMock
+import homeassistant
from homeassistant.components import geo_location
+from homeassistant.components.geo_location import ATTR_SOURCE
from homeassistant.components.geo_location.geo_json_events import \
SCAN_INTERVAL, ATTR_EXTERNAL_ID
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_START, \
@@ -84,7 +86,8 @@ class TestGeoJsonPlatform(unittest.TestCase):
assert state.attributes == {
ATTR_EXTERNAL_ID: "1234", ATTR_LATITUDE: -31.0,
ATTR_LONGITUDE: 150.0, ATTR_FRIENDLY_NAME: "Title 1",
- ATTR_UNIT_OF_MEASUREMENT: "km"}
+ ATTR_UNIT_OF_MEASUREMENT: "km",
+ ATTR_SOURCE: 'geo_json_events'}
self.assertAlmostEqual(float(state.state), 15.5)
state = self.hass.states.get("geo_location.title_2")
@@ -93,7 +96,8 @@ class TestGeoJsonPlatform(unittest.TestCase):
assert state.attributes == {
ATTR_EXTERNAL_ID: "2345", ATTR_LATITUDE: -31.1,
ATTR_LONGITUDE: 150.1, ATTR_FRIENDLY_NAME: "Title 2",
- ATTR_UNIT_OF_MEASUREMENT: "km"}
+ ATTR_UNIT_OF_MEASUREMENT: "km",
+ ATTR_SOURCE: 'geo_json_events'}
self.assertAlmostEqual(float(state.state), 20.5)
state = self.hass.states.get("geo_location.title_3")
@@ -102,7 +106,8 @@ class TestGeoJsonPlatform(unittest.TestCase):
assert state.attributes == {
ATTR_EXTERNAL_ID: "3456", ATTR_LATITUDE: -31.2,
ATTR_LONGITUDE: 150.2, ATTR_FRIENDLY_NAME: "Title 3",
- ATTR_UNIT_OF_MEASUREMENT: "km"}
+ ATTR_UNIT_OF_MEASUREMENT: "km",
+ ATTR_SOURCE: 'geo_json_events'}
self.assertAlmostEqual(float(state.state), 25.5)
# Simulate an update - one existing, one new entry,
@@ -134,3 +139,90 @@ class TestGeoJsonPlatform(unittest.TestCase):
all_states = self.hass.states.all()
assert len(all_states) == 0
+
+ @mock.patch('geojson_client.generic_feed.GenericFeed')
+ def test_setup_race_condition(self, mock_feed):
+ """Test a particular race condition experienced."""
+ # 1. Feed returns 1 entry -> Feed manager creates 1 entity.
+ # 2. Feed returns error -> Feed manager removes 1 entity.
+ # However, this stayed on and kept listening for dispatcher signals.
+ # 3. Feed returns 1 entry -> Feed manager creates 1 entity.
+ # 4. Feed returns 1 entry -> Feed manager updates 1 entity.
+ # Internally, the previous entity is updating itself, too.
+ # 5. Feed returns error -> Feed manager removes 1 entity.
+ # There are now 2 entities trying to remove themselves from HA, but
+ # the second attempt fails of course.
+
+ # Set up some mock feed entries for this test.
+ mock_entry_1 = self._generate_mock_feed_entry('1234', 'Title 1', 15.5,
+ (-31.0, 150.0))
+ mock_feed.return_value.update.return_value = 'OK', [mock_entry_1]
+
+ utcnow = dt_util.utcnow()
+ # Patching 'utcnow' to gain more control over the timed update.
+ with patch('homeassistant.util.dt.utcnow', return_value=utcnow):
+ with assert_setup_component(1, geo_location.DOMAIN):
+ self.assertTrue(setup_component(self.hass, geo_location.DOMAIN,
+ CONFIG))
+
+ # This gives us the ability to assert the '_delete_callback'
+ # has been called while still executing it.
+ original_delete_callback = homeassistant.components\
+ .geo_location.geo_json_events.GeoJsonLocationEvent\
+ ._delete_callback
+
+ def mock_delete_callback(entity):
+ original_delete_callback(entity)
+
+ with patch('homeassistant.components.geo_location'
+ '.geo_json_events.GeoJsonLocationEvent'
+ '._delete_callback',
+ side_effect=mock_delete_callback,
+ autospec=True) as mocked_delete_callback:
+
+ # Artificially trigger update.
+ self.hass.bus.fire(EVENT_HOMEASSISTANT_START)
+ # Collect events.
+ self.hass.block_till_done()
+
+ all_states = self.hass.states.all()
+ assert len(all_states) == 1
+
+ # Simulate an update - empty data, removes all entities
+ mock_feed.return_value.update.return_value = 'ERROR', None
+ fire_time_changed(self.hass, utcnow + SCAN_INTERVAL)
+ self.hass.block_till_done()
+
+ assert mocked_delete_callback.call_count == 1
+ all_states = self.hass.states.all()
+ assert len(all_states) == 0
+
+ # Simulate an update - 1 entry
+ mock_feed.return_value.update.return_value = 'OK', [
+ mock_entry_1]
+ fire_time_changed(self.hass, utcnow + 2 * SCAN_INTERVAL)
+ self.hass.block_till_done()
+
+ all_states = self.hass.states.all()
+ assert len(all_states) == 1
+
+ # Simulate an update - 1 entry
+ mock_feed.return_value.update.return_value = 'OK', [
+ mock_entry_1]
+ fire_time_changed(self.hass, utcnow + 3 * SCAN_INTERVAL)
+ self.hass.block_till_done()
+
+ all_states = self.hass.states.all()
+ assert len(all_states) == 1
+
+ # Reset mocked method for the next test.
+ mocked_delete_callback.reset_mock()
+
+ # Simulate an update - empty data, removes all entities
+ mock_feed.return_value.update.return_value = 'ERROR', None
+ fire_time_changed(self.hass, utcnow + 4 * SCAN_INTERVAL)
+ self.hass.block_till_done()
+
+ assert mocked_delete_callback.call_count == 1
+ all_states = self.hass.states.all()
+ assert len(all_states) == 0
diff --git a/tests/components/geo_location/test_init.py b/tests/components/geo_location/test_init.py
index 54efe977bf9..09030354901 100644
--- a/tests/components/geo_location/test_init.py
+++ b/tests/components/geo_location/test_init.py
@@ -1,4 +1,6 @@
"""The tests for the geo location component."""
+import pytest
+
from homeassistant.components import geo_location
from homeassistant.components.geo_location import GeoLocationEvent
from homeassistant.setup import async_setup_component
@@ -18,3 +20,5 @@ async def test_event(hass):
assert entity.distance is None
assert entity.latitude is None
assert entity.longitude is None
+ with pytest.raises(NotImplementedError):
+ assert entity.source is None
diff --git a/tests/components/geo_location/test_nsw_rural_fire_service_feed.py b/tests/components/geo_location/test_nsw_rural_fire_service_feed.py
new file mode 100644
index 00000000000..92c4bae1931
--- /dev/null
+++ b/tests/components/geo_location/test_nsw_rural_fire_service_feed.py
@@ -0,0 +1,175 @@
+"""The tests for the geojson platform."""
+import datetime
+import unittest
+from unittest import mock
+from unittest.mock import patch, MagicMock
+
+from homeassistant.components import geo_location
+from homeassistant.components.geo_location import ATTR_SOURCE
+from homeassistant.components.geo_location.nsw_rural_fire_service_feed import \
+ ATTR_EXTERNAL_ID, SCAN_INTERVAL, ATTR_CATEGORY, ATTR_FIRE, ATTR_LOCATION, \
+ ATTR_COUNCIL_AREA, ATTR_STATUS, ATTR_TYPE, ATTR_SIZE, \
+ ATTR_RESPONSIBLE_AGENCY, ATTR_PUBLICATION_DATE
+from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_START, \
+ CONF_RADIUS, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_FRIENDLY_NAME, \
+ ATTR_UNIT_OF_MEASUREMENT, ATTR_ATTRIBUTION
+from homeassistant.setup import setup_component
+from tests.common import get_test_home_assistant, assert_setup_component, \
+ fire_time_changed
+import homeassistant.util.dt as dt_util
+
+URL = 'http://geo.json.local/geo_json_events.json'
+CONFIG = {
+ geo_location.DOMAIN: [
+ {
+ 'platform': 'nsw_rural_fire_service_feed',
+ CONF_URL: URL,
+ CONF_RADIUS: 200
+ }
+ ]
+}
+
+
+class TestGeoJsonPlatform(unittest.TestCase):
+ """Test the geojson platform."""
+
+ def setUp(self):
+ """Initialize values for this testcase class."""
+ self.hass = get_test_home_assistant()
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ @staticmethod
+ def _generate_mock_feed_entry(external_id, title, distance_to_home,
+ coordinates, category=None, location=None,
+ attribution=None, publication_date=None,
+ council_area=None, status=None,
+ entry_type=None, fire=True, size=None,
+ responsible_agency=None):
+ """Construct a mock feed entry for testing purposes."""
+ feed_entry = MagicMock()
+ feed_entry.external_id = external_id
+ feed_entry.title = title
+ feed_entry.distance_to_home = distance_to_home
+ feed_entry.coordinates = coordinates
+ feed_entry.category = category
+ feed_entry.location = location
+ feed_entry.attribution = attribution
+ feed_entry.publication_date = publication_date
+ feed_entry.council_area = council_area
+ feed_entry.status = status
+ feed_entry.type = entry_type
+ feed_entry.fire = fire
+ feed_entry.size = size
+ feed_entry.responsible_agency = responsible_agency
+ return feed_entry
+
+ @mock.patch('geojson_client.nsw_rural_fire_service_feed.'
+ 'NswRuralFireServiceFeed')
+ def test_setup(self, mock_feed):
+ """Test the general setup of the platform."""
+ # Set up some mock feed entries for this test.
+ mock_entry_1 = self._generate_mock_feed_entry(
+ '1234', 'Title 1', 15.5, (-31.0, 150.0), category='Category 1',
+ location='Location 1', attribution='Attribution 1',
+ publication_date=datetime.datetime(2018, 9, 22, 8, 0,
+ tzinfo=datetime.timezone.utc),
+ council_area='Council Area 1', status='Status 1',
+ entry_type='Type 1', size='Size 1', responsible_agency='Agency 1')
+ mock_entry_2 = self._generate_mock_feed_entry('2345', 'Title 2', 20.5,
+ (-31.1, 150.1),
+ fire=False)
+ mock_entry_3 = self._generate_mock_feed_entry('3456', 'Title 3', 25.5,
+ (-31.2, 150.2))
+ mock_entry_4 = self._generate_mock_feed_entry('4567', 'Title 4', 12.5,
+ (-31.3, 150.3))
+ mock_feed.return_value.update.return_value = 'OK', [mock_entry_1,
+ mock_entry_2,
+ mock_entry_3]
+
+ utcnow = dt_util.utcnow()
+ # Patching 'utcnow' to gain more control over the timed update.
+ with patch('homeassistant.util.dt.utcnow', return_value=utcnow):
+ with assert_setup_component(1, geo_location.DOMAIN):
+ self.assertTrue(setup_component(self.hass, geo_location.DOMAIN,
+ CONFIG))
+ # Artificially trigger update.
+ self.hass.bus.fire(EVENT_HOMEASSISTANT_START)
+ # Collect events.
+ self.hass.block_till_done()
+
+ all_states = self.hass.states.all()
+ assert len(all_states) == 3
+
+ state = self.hass.states.get("geo_location.title_1")
+ self.assertIsNotNone(state)
+ assert state.name == "Title 1"
+ assert state.attributes == {
+ ATTR_EXTERNAL_ID: "1234", ATTR_LATITUDE: -31.0,
+ ATTR_LONGITUDE: 150.0, ATTR_FRIENDLY_NAME: "Title 1",
+ ATTR_CATEGORY: "Category 1", ATTR_LOCATION: "Location 1",
+ ATTR_ATTRIBUTION: "Attribution 1",
+ ATTR_PUBLICATION_DATE:
+ datetime.datetime(2018, 9, 22, 8, 0,
+ tzinfo=datetime.timezone.utc),
+ ATTR_FIRE: True,
+ ATTR_COUNCIL_AREA: 'Council Area 1',
+ ATTR_STATUS: 'Status 1', ATTR_TYPE: 'Type 1',
+ ATTR_SIZE: 'Size 1', ATTR_RESPONSIBLE_AGENCY: 'Agency 1',
+ ATTR_UNIT_OF_MEASUREMENT: "km",
+ ATTR_SOURCE: 'nsw_rural_fire_service_feed'}
+ self.assertAlmostEqual(float(state.state), 15.5)
+
+ state = self.hass.states.get("geo_location.title_2")
+ self.assertIsNotNone(state)
+ assert state.name == "Title 2"
+ assert state.attributes == {
+ ATTR_EXTERNAL_ID: "2345", ATTR_LATITUDE: -31.1,
+ ATTR_LONGITUDE: 150.1, ATTR_FRIENDLY_NAME: "Title 2",
+ ATTR_FIRE: False,
+ ATTR_UNIT_OF_MEASUREMENT: "km",
+ ATTR_SOURCE: 'nsw_rural_fire_service_feed'}
+ self.assertAlmostEqual(float(state.state), 20.5)
+
+ state = self.hass.states.get("geo_location.title_3")
+ self.assertIsNotNone(state)
+ assert state.name == "Title 3"
+ assert state.attributes == {
+ ATTR_EXTERNAL_ID: "3456", ATTR_LATITUDE: -31.2,
+ ATTR_LONGITUDE: 150.2, ATTR_FRIENDLY_NAME: "Title 3",
+ ATTR_FIRE: True,
+ ATTR_UNIT_OF_MEASUREMENT: "km",
+ ATTR_SOURCE: 'nsw_rural_fire_service_feed'}
+ self.assertAlmostEqual(float(state.state), 25.5)
+
+ # Simulate an update - one existing, one new entry,
+ # one outdated entry
+ mock_feed.return_value.update.return_value = 'OK', [
+ mock_entry_1, mock_entry_4, mock_entry_3]
+ fire_time_changed(self.hass, utcnow + SCAN_INTERVAL)
+ self.hass.block_till_done()
+
+ all_states = self.hass.states.all()
+ assert len(all_states) == 3
+
+ # Simulate an update - empty data, but successful update,
+ # so no changes to entities.
+ mock_feed.return_value.update.return_value = 'OK_NO_DATA', None
+ # mock_restdata.return_value.data = None
+ fire_time_changed(self.hass, utcnow +
+ 2 * SCAN_INTERVAL)
+ self.hass.block_till_done()
+
+ all_states = self.hass.states.all()
+ assert len(all_states) == 3
+
+ # Simulate an update - empty data, removes all entities
+ mock_feed.return_value.update.return_value = 'ERROR', None
+ fire_time_changed(self.hass, utcnow +
+ 2 * SCAN_INTERVAL)
+ self.hass.block_till_done()
+
+ all_states = self.hass.states.all()
+ assert len(all_states) == 0
diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py
index 55c8a7778cb..104d1427dc9 100644
--- a/tests/components/group/test_init.py
+++ b/tests/components/group/test_init.py
@@ -108,6 +108,36 @@ class TestComponentsGroup(unittest.TestCase):
group_state = self.hass.states.get(test_group.entity_id)
self.assertEqual(STATE_ON, group_state.state)
+ def test_allgroup_stays_off_if_all_are_off_and_one_turns_on(self):
+ """Group with all: true, stay off if one device turns on."""
+ self.hass.states.set('light.Bowl', STATE_OFF)
+ self.hass.states.set('light.Ceiling', STATE_OFF)
+ test_group = group.Group.create_group(
+ self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False,
+ mode=True)
+
+ # Turn one on
+ self.hass.states.set('light.Ceiling', STATE_ON)
+ self.hass.block_till_done()
+
+ group_state = self.hass.states.get(test_group.entity_id)
+ self.assertEqual(STATE_OFF, group_state.state)
+
+ def test_allgroup_turn_on_if_last_turns_on(self):
+ """Group with all: true, turn on if all devices are on."""
+ self.hass.states.set('light.Bowl', STATE_ON)
+ self.hass.states.set('light.Ceiling', STATE_OFF)
+ test_group = group.Group.create_group(
+ self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False,
+ mode=True)
+
+ # Turn one on
+ self.hass.states.set('light.Ceiling', STATE_ON)
+ self.hass.block_till_done()
+
+ group_state = self.hass.states.get(test_group.entity_id)
+ self.assertEqual(STATE_ON, group_state.state)
+
def test_is_on(self):
"""Test is_on method."""
self.hass.states.set('light.Bowl', STATE_ON)
diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py
index 55e02de7526..95829435d0e 100644
--- a/tests/components/homekit/conftest.py
+++ b/tests/components/homekit/conftest.py
@@ -3,6 +3,9 @@ from unittest.mock import patch
import pytest
+from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED
+from homeassistant.core import callback as ha_callback
+
from pyhap.accessory_driver import AccessoryDriver
@@ -14,3 +17,13 @@ def hk_driver():
patch('pyhap.accessory_driver.HAPServer'), \
patch('pyhap.accessory_driver.AccessoryDriver.publish'):
return AccessoryDriver(pincode=b'123-45-678', address='127.0.0.1')
+
+
+@pytest.fixture
+def events(hass):
+ """Yield caught homekit_changed events."""
+ events = []
+ hass.bus.async_listen(
+ EVENT_HOMEKIT_CHANGED,
+ ha_callback(lambda e: events.append(e)))
+ yield events
diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py
index edb1c7175f8..15ab6d7413e 100644
--- a/tests/components/homekit/test_accessories.py
+++ b/tests/components/homekit/test_accessories.py
@@ -10,14 +10,17 @@ import pytest
from homeassistant.components.homekit.accessories import (
debounce, HomeAccessory, HomeBridge, HomeDriver)
from homeassistant.components.homekit.const import (
+ ATTR_DISPLAY_NAME, ATTR_VALUE,
BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_FIRMWARE_REVISION,
CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER,
MANUFACTURER, SERV_ACCESSORY_INFO)
from homeassistant.const import (
- __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_NOW,
- EVENT_TIME_CHANGED)
+ __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID,
+ ATTR_SERVICE, ATTR_NOW, EVENT_TIME_CHANGED)
import homeassistant.util.dt as dt_util
+from tests.common import async_mock_service
+
async def test_debounce(hass):
"""Test add_timeout decorator function."""
@@ -146,6 +149,37 @@ async def test_battery_service(hass, hk_driver):
assert acc._char_charging.value == 0
+async def test_call_service(hass, hk_driver, events):
+ """Test call_service method."""
+ entity_id = 'homekit.accessory'
+ hass.states.async_set(entity_id, None)
+ await hass.async_block_till_done()
+
+ acc = HomeAccessory(hass, hk_driver, 'Home Accessory', entity_id, 2, None)
+ call_service = async_mock_service(hass, 'cover', 'open_cover')
+
+ test_domain = 'cover'
+ test_service = 'open_cover'
+ test_value = 'value'
+
+ await acc.async_call_service(
+ test_domain, test_service, {ATTR_ENTITY_ID: entity_id}, test_value)
+ await hass.async_block_till_done()
+
+ assert len(events) == 1
+ assert events[0].data == {
+ ATTR_ENTITY_ID: acc.entity_id,
+ ATTR_DISPLAY_NAME: acc.display_name,
+ ATTR_SERVICE: test_service,
+ ATTR_VALUE: test_value
+ }
+
+ assert len(call_service) == 1
+ assert call_service[0].domain == test_domain
+ assert call_service[0].service == test_service
+ assert call_service[0].data == {ATTR_ENTITY_ID: entity_id}
+
+
def test_home_bridge(hk_driver):
"""Test HomeBridge class."""
bridge = HomeBridge('hass', hk_driver, BRIDGE_NAME)
diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py
index d5552cce82c..7d303c38e93 100644
--- a/tests/components/homekit/test_get_accessories.py
+++ b/tests/components/homekit/test_get_accessories.py
@@ -69,6 +69,7 @@ def test_customize_options(config, name):
('Thermostat', 'climate.test', 'auto',
{ATTR_SUPPORTED_FEATURES: climate.SUPPORT_TARGET_TEMPERATURE_LOW |
climate.SUPPORT_TARGET_TEMPERATURE_HIGH}, {}),
+ ('WaterHeater', 'water_heater.test', 'auto', {}, {}),
])
def test_types(type_name, entity_id, state, attrs, config):
"""Test if types are associated correctly."""
diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py
index 04ed5df5702..c32abaef0dd 100644
--- a/tests/components/homekit/test_type_covers.py
+++ b/tests/components/homekit/test_type_covers.py
@@ -5,6 +5,7 @@ import pytest
from homeassistant.components.cover import (
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP)
+from homeassistant.components.homekit.const import ATTR_VALUE
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES,
STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN)
@@ -28,7 +29,7 @@ def cls():
patcher.stop()
-async def test_garage_door_open_close(hass, hk_driver, cls):
+async def test_garage_door_open_close(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = 'cover.garage_door'
@@ -73,6 +74,8 @@ async def test_garage_door_open_close(hass, hk_driver, cls):
assert call_close_cover[0].data[ATTR_ENTITY_ID] == entity_id
assert acc.char_current_state.value == 2
assert acc.char_target_state.value == 1
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is None
hass.states.async_set(entity_id, STATE_CLOSED)
await hass.async_block_till_done()
@@ -83,9 +86,11 @@ async def test_garage_door_open_close(hass, hk_driver, cls):
assert call_open_cover[0].data[ATTR_ENTITY_ID] == entity_id
assert acc.char_current_state.value == 3
assert acc.char_target_state.value == 0
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] is None
-async def test_window_set_cover_position(hass, hk_driver, cls):
+async def test_window_set_cover_position(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = 'cover.window'
@@ -123,6 +128,8 @@ async def test_window_set_cover_position(hass, hk_driver, cls):
assert call_set_cover_position[0].data[ATTR_POSITION] == 25
assert acc.char_current_position.value == 50
assert acc.char_target_position.value == 25
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] == 25
await hass.async_add_job(acc.char_target_position.client_update_value, 75)
await hass.async_block_till_done()
@@ -131,9 +138,11 @@ async def test_window_set_cover_position(hass, hk_driver, cls):
assert call_set_cover_position[1].data[ATTR_POSITION] == 75
assert acc.char_current_position.value == 50
assert acc.char_target_position.value == 75
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] == 75
-async def test_window_open_close(hass, hk_driver, cls):
+async def test_window_open_close(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = 'cover.window'
@@ -178,6 +187,8 @@ async def test_window_open_close(hass, hk_driver, cls):
assert acc.char_current_position.value == 0
assert acc.char_target_position.value == 0
assert acc.char_position_state.value == 2
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.char_target_position.client_update_value, 90)
await hass.async_block_till_done()
@@ -186,6 +197,8 @@ async def test_window_open_close(hass, hk_driver, cls):
assert acc.char_current_position.value == 100
assert acc.char_target_position.value == 100
assert acc.char_position_state.value == 2
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.char_target_position.client_update_value, 55)
await hass.async_block_till_done()
@@ -194,9 +207,11 @@ async def test_window_open_close(hass, hk_driver, cls):
assert acc.char_current_position.value == 100
assert acc.char_target_position.value == 100
assert acc.char_position_state.value == 2
+ assert len(events) == 3
+ assert events[-1].data[ATTR_VALUE] is None
-async def test_window_open_close_stop(hass, hk_driver, cls):
+async def test_window_open_close_stop(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = 'cover.window'
@@ -217,6 +232,8 @@ async def test_window_open_close_stop(hass, hk_driver, cls):
assert acc.char_current_position.value == 0
assert acc.char_target_position.value == 0
assert acc.char_position_state.value == 2
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.char_target_position.client_update_value, 90)
await hass.async_block_till_done()
@@ -225,6 +242,8 @@ async def test_window_open_close_stop(hass, hk_driver, cls):
assert acc.char_current_position.value == 100
assert acc.char_target_position.value == 100
assert acc.char_position_state.value == 2
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.char_target_position.client_update_value, 55)
await hass.async_block_till_done()
@@ -233,3 +252,5 @@ async def test_window_open_close_stop(hass, hk_driver, cls):
assert acc.char_current_position.value == 50
assert acc.char_target_position.value == 50
assert acc.char_position_state.value == 2
+ assert len(events) == 3
+ assert events[-1].data[ATTR_VALUE] is None
diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py
index 87a481ff06f..27b6cec0790 100644
--- a/tests/components/homekit/test_type_fans.py
+++ b/tests/components/homekit/test_type_fans.py
@@ -6,6 +6,7 @@ import pytest
from homeassistant.components.fan import (
ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE,
DOMAIN, SUPPORT_DIRECTION, SUPPORT_OSCILLATE)
+from homeassistant.components.homekit.const import ATTR_VALUE
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF,
STATE_UNKNOWN)
@@ -26,7 +27,7 @@ def cls():
patcher.stop()
-async def test_fan_basic(hass, hk_driver, cls):
+async def test_fan_basic(hass, hk_driver, cls, events):
"""Test fan with char state."""
entity_id = 'fan.demo'
@@ -62,6 +63,8 @@ async def test_fan_basic(hass, hk_driver, cls):
await hass.async_block_till_done()
assert call_turn_on
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is None
hass.states.async_set(entity_id, STATE_ON)
await hass.async_block_till_done()
@@ -70,9 +73,11 @@ async def test_fan_basic(hass, hk_driver, cls):
await hass.async_block_till_done()
assert call_turn_off
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] is None
-async def test_fan_direction(hass, hk_driver, cls):
+async def test_fan_direction(hass, hk_driver, cls, events):
"""Test fan with direction."""
entity_id = 'fan.demo'
@@ -101,15 +106,19 @@ async def test_fan_direction(hass, hk_driver, cls):
assert call_set_direction[0]
assert call_set_direction[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_direction[0].data[ATTR_DIRECTION] == DIRECTION_FORWARD
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] == DIRECTION_FORWARD
await hass.async_add_job(acc.char_direction.client_update_value, 1)
await hass.async_block_till_done()
assert call_set_direction[1]
assert call_set_direction[1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_direction[1].data[ATTR_DIRECTION] == DIRECTION_REVERSE
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] == DIRECTION_REVERSE
-async def test_fan_oscillate(hass, hk_driver, cls):
+async def test_fan_oscillate(hass, hk_driver, cls, events):
"""Test fan with oscillate."""
entity_id = 'fan.demo'
@@ -136,9 +145,13 @@ async def test_fan_oscillate(hass, hk_driver, cls):
assert call_oscillate[0]
assert call_oscillate[0].data[ATTR_ENTITY_ID] == entity_id
assert call_oscillate[0].data[ATTR_OSCILLATING] is False
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is False
await hass.async_add_job(acc.char_swing.client_update_value, 1)
await hass.async_block_till_done()
assert call_oscillate[1]
assert call_oscillate[1].data[ATTR_ENTITY_ID] == entity_id
assert call_oscillate[1].data[ATTR_OSCILLATING] is True
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] is True
diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py
index aab6274f484..c540952017b 100644
--- a/tests/components/homekit/test_type_lights.py
+++ b/tests/components/homekit/test_type_lights.py
@@ -3,6 +3,7 @@ from collections import namedtuple
import pytest
+from homeassistant.components.homekit.const import ATTR_VALUE
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR,
DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR)
@@ -26,7 +27,7 @@ def cls():
patcher.stop()
-async def test_light_basic(hass, hk_driver, cls):
+async def test_light_basic(hass, hk_driver, cls, events):
"""Test light with char state."""
entity_id = 'light.demo'
@@ -62,6 +63,8 @@ async def test_light_basic(hass, hk_driver, cls):
await hass.async_block_till_done()
assert call_turn_on
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is None
hass.states.async_set(entity_id, STATE_ON)
await hass.async_block_till_done()
@@ -70,9 +73,11 @@ async def test_light_basic(hass, hk_driver, cls):
await hass.async_block_till_done()
assert call_turn_off
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] is None
-async def test_light_brightness(hass, hk_driver, cls):
+async def test_light_brightness(hass, hk_driver, cls, events):
"""Test light with brightness."""
entity_id = 'light.demo'
@@ -101,6 +106,8 @@ async def test_light_brightness(hass, hk_driver, cls):
assert call_turn_on[0]
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] == 'brightness at 20%'
await hass.async_add_job(acc.char_on.client_update_value, 1)
await hass.async_add_job(acc.char_brightness.client_update_value, 40)
@@ -108,15 +115,19 @@ async def test_light_brightness(hass, hk_driver, cls):
assert call_turn_on[1]
assert call_turn_on[1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[1].data[ATTR_BRIGHTNESS_PCT] == 40
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] == 'brightness at 40%'
await hass.async_add_job(acc.char_on.client_update_value, 1)
await hass.async_add_job(acc.char_brightness.client_update_value, 0)
await hass.async_block_till_done()
assert call_turn_off
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 3
+ assert events[-1].data[ATTR_VALUE] is None
-async def test_light_color_temperature(hass, hk_driver, cls):
+async def test_light_color_temperature(hass, hk_driver, cls, events):
"""Test light with color temperature."""
entity_id = 'light.demo'
@@ -141,9 +152,11 @@ async def test_light_color_temperature(hass, hk_driver, cls):
assert call_turn_on
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] == 'color temperature at 250'
-async def test_light_rgb_color(hass, hk_driver, cls):
+async def test_light_rgb_color(hass, hk_driver, cls, events):
"""Test light with rgb_color."""
entity_id = 'light.demo'
@@ -170,3 +183,5 @@ async def test_light_rgb_color(hass, hk_driver, cls):
assert call_turn_on
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75)
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] == 'set color at (145, 75)'
diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py
index 8f18a591019..8132099bd3e 100644
--- a/tests/components/homekit/test_type_locks.py
+++ b/tests/components/homekit/test_type_locks.py
@@ -1,6 +1,7 @@
"""Test different accessory types: Locks."""
import pytest
+from homeassistant.components.homekit.const import ATTR_VALUE
from homeassistant.components.homekit.type_locks import Lock
from homeassistant.components.lock import DOMAIN
from homeassistant.const import (
@@ -9,7 +10,7 @@ from homeassistant.const import (
from tests.common import async_mock_service
-async def test_lock_unlock(hass, hk_driver):
+async def test_lock_unlock(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly."""
code = '1234'
config = {ATTR_CODE: code}
@@ -56,6 +57,8 @@ async def test_lock_unlock(hass, hk_driver):
assert call_lock[0].data[ATTR_ENTITY_ID] == entity_id
assert call_lock[0].data[ATTR_CODE] == code
assert acc.char_target_state.value == 1
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.char_target_state.client_update_value, 0)
await hass.async_block_till_done()
@@ -63,10 +66,12 @@ async def test_lock_unlock(hass, hk_driver):
assert call_unlock[0].data[ATTR_ENTITY_ID] == entity_id
assert call_unlock[0].data[ATTR_CODE] == code
assert acc.char_target_state.value == 0
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] is None
@pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}])
-async def test_no_code(hass, hk_driver, config):
+async def test_no_code(hass, hk_driver, config, events):
"""Test accessory if lock doesn't require a code."""
entity_id = 'lock.kitchen_door'
@@ -77,9 +82,12 @@ async def test_no_code(hass, hk_driver, config):
# Set from HomeKit
call_lock = async_mock_service(hass, DOMAIN, 'lock')
+ acc.char_target_state.value = 0
await hass.async_add_job(acc.char_target_state.client_update_value, 1)
await hass.async_block_till_done()
assert call_lock
assert call_lock[0].data[ATTR_ENTITY_ID] == entity_id
assert ATTR_CODE not in call_lock[0].data
assert acc.char_target_state.value == 1
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is None
diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py
index 681cbba7252..745e4c162bc 100644
--- a/tests/components/homekit/test_type_media_players.py
+++ b/tests/components/homekit/test_type_media_players.py
@@ -1,8 +1,8 @@
"""Test different accessory types: Media Players."""
from homeassistant.components.homekit.const import (
- CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP,
- FEATURE_TOGGLE_MUTE)
+ ATTR_VALUE, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE,
+ FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE)
from homeassistant.components.homekit.type_media_players import MediaPlayer
from homeassistant.components.media_player import (
ATTR_MEDIA_VOLUME_MUTED, DOMAIN)
@@ -13,7 +13,7 @@ from homeassistant.const import (
from tests.common import async_mock_service
-async def test_media_player_set_state(hass, hk_driver):
+async def test_media_player_set_state(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly."""
config = {CONF_FEATURE_LIST: {
FEATURE_ON_OFF: None, FEATURE_PLAY_PAUSE: None,
@@ -29,32 +29,32 @@ async def test_media_player_set_state(hass, hk_driver):
assert acc.aid == 2
assert acc.category == 8 # Switch
- assert acc.chars[FEATURE_ON_OFF].value == 0
- assert acc.chars[FEATURE_PLAY_PAUSE].value == 0
- assert acc.chars[FEATURE_PLAY_STOP].value == 0
- assert acc.chars[FEATURE_TOGGLE_MUTE].value == 0
+ assert acc.chars[FEATURE_ON_OFF].value is False
+ assert acc.chars[FEATURE_PLAY_PAUSE].value is False
+ assert acc.chars[FEATURE_PLAY_STOP].value is False
+ assert acc.chars[FEATURE_TOGGLE_MUTE].value is False
hass.states.async_set(entity_id, STATE_ON, {ATTR_MEDIA_VOLUME_MUTED: True})
await hass.async_block_till_done()
- assert acc.chars[FEATURE_ON_OFF].value == 1
- assert acc.chars[FEATURE_TOGGLE_MUTE].value == 1
+ assert acc.chars[FEATURE_ON_OFF].value is True
+ assert acc.chars[FEATURE_TOGGLE_MUTE].value is True
hass.states.async_set(entity_id, STATE_OFF)
await hass.async_block_till_done()
- assert acc.chars[FEATURE_ON_OFF].value == 0
+ assert acc.chars[FEATURE_ON_OFF].value is False
hass.states.async_set(entity_id, STATE_PLAYING)
await hass.async_block_till_done()
- assert acc.chars[FEATURE_PLAY_PAUSE].value == 1
- assert acc.chars[FEATURE_PLAY_STOP].value == 1
+ assert acc.chars[FEATURE_PLAY_PAUSE].value is True
+ assert acc.chars[FEATURE_PLAY_STOP].value is True
hass.states.async_set(entity_id, STATE_PAUSED)
await hass.async_block_till_done()
- assert acc.chars[FEATURE_PLAY_PAUSE].value == 0
+ assert acc.chars[FEATURE_PLAY_PAUSE].value is False
hass.states.async_set(entity_id, STATE_IDLE)
await hass.async_block_till_done()
- assert acc.chars[FEATURE_PLAY_STOP].value == 0
+ assert acc.chars[FEATURE_PLAY_STOP].value is False
# Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on')
@@ -64,41 +64,54 @@ async def test_media_player_set_state(hass, hk_driver):
call_media_stop = async_mock_service(hass, DOMAIN, 'media_stop')
call_toggle_mute = async_mock_service(hass, DOMAIN, 'volume_mute')
+ acc.chars[FEATURE_ON_OFF].value = False
await hass.async_add_job(acc.chars[FEATURE_ON_OFF]
.client_update_value, True)
await hass.async_block_till_done()
assert call_turn_on
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.chars[FEATURE_ON_OFF]
.client_update_value, False)
await hass.async_block_till_done()
assert call_turn_off
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.chars[FEATURE_PLAY_PAUSE]
.client_update_value, True)
await hass.async_block_till_done()
assert call_media_play
assert call_media_play[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 3
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.chars[FEATURE_PLAY_PAUSE]
.client_update_value, False)
await hass.async_block_till_done()
assert call_media_pause
assert call_media_pause[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 4
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.chars[FEATURE_PLAY_STOP]
.client_update_value, True)
await hass.async_block_till_done()
assert call_media_play
assert call_media_play[1].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 5
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.chars[FEATURE_PLAY_STOP]
.client_update_value, False)
await hass.async_block_till_done()
assert call_media_stop
assert call_media_stop[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 6
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.chars[FEATURE_TOGGLE_MUTE]
.client_update_value, True)
@@ -106,6 +119,8 @@ async def test_media_player_set_state(hass, hk_driver):
assert call_toggle_mute
assert call_toggle_mute[0].data[ATTR_ENTITY_ID] == entity_id
assert call_toggle_mute[0].data[ATTR_MEDIA_VOLUME_MUTED] is True
+ assert len(events) == 7
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.chars[FEATURE_TOGGLE_MUTE]
.client_update_value, False)
@@ -113,3 +128,5 @@ async def test_media_player_set_state(hass, hk_driver):
assert call_toggle_mute
assert call_toggle_mute[1].data[ATTR_ENTITY_ID] == entity_id
assert call_toggle_mute[1].data[ATTR_MEDIA_VOLUME_MUTED] is False
+ assert len(events) == 8
+ assert events[-1].data[ATTR_VALUE] is None
diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py
index 3ddce0f36eb..3753a1aa433 100644
--- a/tests/components/homekit/test_type_security_systems.py
+++ b/tests/components/homekit/test_type_security_systems.py
@@ -2,6 +2,7 @@
import pytest
from homeassistant.components.alarm_control_panel import DOMAIN
+from homeassistant.components.homekit.const import ATTR_VALUE
from homeassistant.components.homekit.type_security_systems import \
SecuritySystem
from homeassistant.const import (
@@ -12,7 +13,7 @@ from homeassistant.const import (
from tests.common import async_mock_service
-async def test_switch_set_state(hass, hk_driver):
+async def test_switch_set_state(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly."""
code = '1234'
config = {ATTR_CODE: code}
@@ -72,6 +73,8 @@ async def test_switch_set_state(hass, hk_driver):
assert call_arm_home[0].data[ATTR_ENTITY_ID] == entity_id
assert call_arm_home[0].data[ATTR_CODE] == code
assert acc.char_target_state.value == 0
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.char_target_state.client_update_value, 1)
await hass.async_block_till_done()
@@ -79,6 +82,8 @@ async def test_switch_set_state(hass, hk_driver):
assert call_arm_away[0].data[ATTR_ENTITY_ID] == entity_id
assert call_arm_away[0].data[ATTR_CODE] == code
assert acc.char_target_state.value == 1
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.char_target_state.client_update_value, 2)
await hass.async_block_till_done()
@@ -86,6 +91,8 @@ async def test_switch_set_state(hass, hk_driver):
assert call_arm_night[0].data[ATTR_ENTITY_ID] == entity_id
assert call_arm_night[0].data[ATTR_CODE] == code
assert acc.char_target_state.value == 2
+ assert len(events) == 3
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.char_target_state.client_update_value, 3)
await hass.async_block_till_done()
@@ -93,10 +100,12 @@ async def test_switch_set_state(hass, hk_driver):
assert call_disarm[0].data[ATTR_ENTITY_ID] == entity_id
assert call_disarm[0].data[ATTR_CODE] == code
assert acc.char_target_state.value == 3
+ assert len(events) == 4
+ assert events[-1].data[ATTR_VALUE] is None
@pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}])
-async def test_no_alarm_code(hass, hk_driver, config):
+async def test_no_alarm_code(hass, hk_driver, config, events):
"""Test accessory if security_system doesn't require an alarm_code."""
entity_id = 'alarm_control_panel.test'
@@ -114,3 +123,5 @@ async def test_no_alarm_code(hass, hk_driver, config):
assert call_arm_home[0].data[ATTR_ENTITY_ID] == entity_id
assert ATTR_CODE not in call_arm_home[0].data
assert acc.char_target_state.value == 0
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is None
diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py
index bc44a93884a..d170647d492 100644
--- a/tests/components/homekit/test_type_switches.py
+++ b/tests/components/homekit/test_type_switches.py
@@ -2,7 +2,7 @@
import pytest
from homeassistant.components.homekit.const import (
- TYPE_FAUCET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_VALVE)
+ ATTR_VALUE, TYPE_FAUCET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_VALVE)
from homeassistant.components.homekit.type_switches import (
Outlet, Switch, Valve)
from homeassistant.const import ATTR_ENTITY_ID, CONF_TYPE, STATE_OFF, STATE_ON
@@ -11,7 +11,7 @@ from homeassistant.core import split_entity_id
from tests.common import async_mock_service
-async def test_outlet_set_state(hass, hk_driver):
+async def test_outlet_set_state(hass, hk_driver, events):
"""Test if Outlet accessory and HA are updated accordingly."""
entity_id = 'switch.outlet_test'
@@ -43,11 +43,15 @@ async def test_outlet_set_state(hass, hk_driver):
await hass.async_block_till_done()
assert call_turn_on
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.char_on.client_update_value, False)
await hass.async_block_till_done()
assert call_turn_off
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] is None
@pytest.mark.parametrize('entity_id', [
@@ -57,7 +61,7 @@ async def test_outlet_set_state(hass, hk_driver):
'script.test',
'switch.test',
])
-async def test_switch_set_state(hass, hk_driver, entity_id):
+async def test_switch_set_state(hass, hk_driver, entity_id, events):
"""Test if accessory and HA are updated accordingly."""
domain = split_entity_id(entity_id)[0]
@@ -88,14 +92,18 @@ async def test_switch_set_state(hass, hk_driver, entity_id):
await hass.async_block_till_done()
assert call_turn_on
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.char_on.client_update_value, False)
await hass.async_block_till_done()
assert call_turn_off
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] is None
-async def test_valve_set_state(hass, hk_driver):
+async def test_valve_set_state(hass, hk_driver, events):
"""Test if Valve accessory and HA are updated accordingly."""
entity_id = 'switch.valve_test'
@@ -154,9 +162,13 @@ async def test_valve_set_state(hass, hk_driver):
assert acc.char_in_use.value is True
assert call_turn_on
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_job(acc.char_active.client_update_value, False)
await hass.async_block_till_done()
assert acc.char_in_use.value is False
assert call_turn_off
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] is None
diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py
index 687a9e9513c..795cb5db7d2 100644
--- a/tests/components/homekit/test_type_thermostats.py
+++ b/tests/components/homekit/test_type_thermostats.py
@@ -5,15 +5,18 @@ from unittest.mock import patch
import pytest
from homeassistant.components.climate import (
- ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TEMPERATURE,
+ ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE,
- ATTR_OPERATION_LIST, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN,
- STATE_AUTO, STATE_COOL, STATE_HEAT)
+ ATTR_OPERATION_LIST, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP,
+ DOMAIN as DOMAIN_CLIMATE, STATE_AUTO, STATE_COOL, STATE_HEAT)
from homeassistant.components.homekit.const import (
+ ATTR_VALUE, DEFAULT_MAX_TEMP_WATER_HEATER, DEFAULT_MIN_TEMP_WATER_HEATER,
PROP_MAX_VALUE, PROP_MIN_VALUE)
+from homeassistant.components.water_heater import (
+ DOMAIN as DOMAIN_WATER_HEATER)
from homeassistant.const import (
- ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_TEMPERATURE_UNIT, STATE_OFF,
- TEMP_FAHRENHEIT)
+ ATTR_ENTITY_ID, ATTR_TEMPERATURE, ATTR_SUPPORTED_FEATURES,
+ CONF_TEMPERATURE_UNIT, STATE_OFF, TEMP_FAHRENHEIT)
from tests.common import async_mock_service
from tests.components.homekit.common import patch_debounce
@@ -25,13 +28,14 @@ def cls():
patcher = patch_debounce()
patcher.start()
_import = __import__('homeassistant.components.homekit.type_thermostats',
- fromlist=['Thermostat'])
- patcher_tuple = namedtuple('Cls', ['thermostat'])
- yield patcher_tuple(thermostat=_import.Thermostat)
+ fromlist=['Thermostat', 'WaterHeater'])
+ patcher_tuple = namedtuple('Cls', ['thermostat', 'water_heater'])
+ yield patcher_tuple(thermostat=_import.Thermostat,
+ water_heater=_import.WaterHeater)
patcher.stop()
-async def test_default_thermostat(hass, hk_driver, cls):
+async def test_thermostat(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = 'climate.test'
@@ -147,8 +151,9 @@ async def test_default_thermostat(hass, hk_driver, cls):
assert acc.char_display_units.value == 0
# Set from HomeKit
- call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature')
- call_set_operation_mode = async_mock_service(hass, DOMAIN,
+ call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE,
+ 'set_temperature')
+ call_set_operation_mode = async_mock_service(hass, DOMAIN_CLIMATE,
'set_operation_mode')
await hass.async_add_job(acc.char_target_temp.client_update_value, 19.0)
@@ -157,6 +162,8 @@ async def test_default_thermostat(hass, hk_driver, cls):
assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 19.0
assert acc.char_target_temp.value == 19.0
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] == '19.0°C'
await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1)
await hass.async_block_till_done()
@@ -164,9 +171,11 @@ async def test_default_thermostat(hass, hk_driver, cls):
assert call_set_operation_mode[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_operation_mode[0].data[ATTR_OPERATION_MODE] == STATE_HEAT
assert acc.char_target_heat_cool.value == 1
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] == STATE_HEAT
-async def test_auto_thermostat(hass, hk_driver, cls):
+async def test_thermostat_auto(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = 'climate.test'
@@ -229,7 +238,8 @@ async def test_auto_thermostat(hass, hk_driver, cls):
assert acc.char_display_units.value == 0
# Set from HomeKit
- call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature')
+ call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE,
+ 'set_temperature')
await hass.async_add_job(
acc.char_heating_thresh_temp.client_update_value, 20.0)
@@ -238,6 +248,8 @@ async def test_auto_thermostat(hass, hk_driver, cls):
assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 20.0
assert acc.char_heating_thresh_temp.value == 20.0
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] == 'heating threshold 20.0°C'
await hass.async_add_job(
acc.char_cooling_thresh_temp.client_update_value, 25.0)
@@ -246,9 +258,11 @@ async def test_auto_thermostat(hass, hk_driver, cls):
assert call_set_temperature[1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 25.0
assert acc.char_cooling_thresh_temp.value == 25.0
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] == 'cooling threshold 25.0°C'
-async def test_power_state(hass, hk_driver, cls):
+async def test_thermostat_power_state(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = 'climate.test'
@@ -284,9 +298,9 @@ async def test_power_state(hass, hk_driver, cls):
assert acc.char_target_heat_cool.value == 0
# Set from HomeKit
- call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on')
- call_turn_off = async_mock_service(hass, DOMAIN, 'turn_off')
- call_set_operation_mode = async_mock_service(hass, DOMAIN,
+ call_turn_on = async_mock_service(hass, DOMAIN_CLIMATE, 'turn_on')
+ call_turn_off = async_mock_service(hass, DOMAIN_CLIMATE, 'turn_off')
+ call_set_operation_mode = async_mock_service(hass, DOMAIN_CLIMATE,
'set_operation_mode')
await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1)
@@ -297,15 +311,19 @@ async def test_power_state(hass, hk_driver, cls):
assert call_set_operation_mode[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_operation_mode[0].data[ATTR_OPERATION_MODE] == STATE_HEAT
assert acc.char_target_heat_cool.value == 1
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] == STATE_HEAT
await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 0)
await hass.async_block_till_done()
assert call_turn_off
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
assert acc.char_target_heat_cool.value == 0
+ assert len(events) == 3
+ assert events[-1].data[ATTR_VALUE] is None
-async def test_thermostat_fahrenheit(hass, hk_driver, cls):
+async def test_thermostat_fahrenheit(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = 'climate.test'
@@ -332,7 +350,8 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls):
assert acc.char_display_units.value == 1
# Set from HomeKit
- call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature')
+ call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE,
+ 'set_temperature')
await hass.async_add_job(
acc.char_cooling_thresh_temp.client_update_value, 23)
@@ -341,6 +360,8 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls):
assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 73.4
assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 68
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] == 'cooling threshold 73.4°F'
await hass.async_add_job(
acc.char_heating_thresh_temp.client_update_value, 22)
@@ -349,15 +370,19 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls):
assert call_set_temperature[1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 73.4
assert call_set_temperature[1].data[ATTR_TARGET_TEMP_LOW] == 71.6
+ assert len(events) == 2
+ assert events[-1].data[ATTR_VALUE] == 'heating threshold 71.6°F'
await hass.async_add_job(acc.char_target_temp.client_update_value, 24.0)
await hass.async_block_till_done()
assert call_set_temperature[2]
assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.2
+ assert len(events) == 3
+ assert events[-1].data[ATTR_VALUE] == '75.2°F'
-async def test_get_temperature_range(hass, hk_driver, cls):
+async def test_thermostat_get_temperature_range(hass, hk_driver, cls):
"""Test if temperature range is evaluated correctly."""
entity_id = 'climate.test'
@@ -375,3 +400,123 @@ async def test_get_temperature_range(hass, hk_driver, cls):
{ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70})
await hass.async_block_till_done()
assert acc.get_temperature_range() == (15.6, 21.1)
+
+
+async def test_water_heater(hass, hk_driver, cls, events):
+ """Test if accessory and HA are updated accordingly."""
+ entity_id = 'water_heater.test'
+
+ hass.states.async_set(entity_id, STATE_HEAT)
+ await hass.async_block_till_done()
+ acc = cls.water_heater(hass, hk_driver, 'WaterHeater', entity_id, 2, None)
+ await hass.async_add_job(acc.run)
+ await hass.async_block_till_done()
+
+ assert acc.aid == 2
+ assert acc.category == 9 # Thermostat
+
+ assert acc.char_current_heat_cool.value == 1 # Heat
+ assert acc.char_target_heat_cool.value == 1 # Heat
+ assert acc.char_current_temp.value == 50.0
+ assert acc.char_target_temp.value == 50.0
+ assert acc.char_display_units.value == 0
+
+ assert acc.char_target_temp.properties[PROP_MAX_VALUE] == \
+ DEFAULT_MAX_TEMP_WATER_HEATER
+ assert acc.char_target_temp.properties[PROP_MIN_VALUE] == \
+ DEFAULT_MIN_TEMP_WATER_HEATER
+
+ hass.states.async_set(entity_id, STATE_HEAT,
+ {ATTR_OPERATION_MODE: STATE_HEAT,
+ ATTR_TEMPERATURE: 56.0})
+ await hass.async_block_till_done()
+ assert acc.char_target_temp.value == 56.0
+ assert acc.char_current_temp.value == 56.0
+ assert acc.char_target_heat_cool.value == 1
+ assert acc.char_current_heat_cool.value == 1
+ assert acc.char_display_units.value == 0
+
+ hass.states.async_set(entity_id, STATE_AUTO,
+ {ATTR_OPERATION_MODE: STATE_AUTO})
+ await hass.async_block_till_done()
+ assert acc.char_target_heat_cool.value == 1
+ assert acc.char_current_heat_cool.value == 1
+
+ # Set from HomeKit
+ call_set_temperature = async_mock_service(hass, DOMAIN_WATER_HEATER,
+ 'set_temperature')
+
+ await hass.async_add_job(acc.char_target_temp.client_update_value, 52.0)
+ await hass.async_block_till_done()
+ assert call_set_temperature
+ assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id
+ assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 52.0
+ assert acc.char_target_temp.value == 52.0
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] == '52.0°C'
+
+ await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 0)
+ await hass.async_block_till_done()
+ assert acc.char_target_heat_cool.value == 1
+
+ await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 2)
+ await hass.async_block_till_done()
+ assert acc.char_target_heat_cool.value == 1
+
+ await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 3)
+ await hass.async_block_till_done()
+ assert acc.char_target_heat_cool.value == 1
+
+
+async def test_water_heater_fahrenheit(hass, hk_driver, cls, events):
+ """Test if accessory and HA are update accordingly."""
+ entity_id = 'water_heater.test'
+
+ hass.states.async_set(entity_id, STATE_HEAT)
+ await hass.async_block_till_done()
+ with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT,
+ new=TEMP_FAHRENHEIT):
+ acc = cls.water_heater(hass, hk_driver, 'WaterHeater',
+ entity_id, 2, None)
+ await hass.async_add_job(acc.run)
+ await hass.async_block_till_done()
+
+ hass.states.async_set(entity_id, STATE_HEAT,
+ {ATTR_TEMPERATURE: 131})
+ await hass.async_block_till_done()
+ assert acc.char_target_temp.value == 55.0
+ assert acc.char_current_temp.value == 55.0
+ assert acc.char_display_units.value == 1
+
+ # Set from HomeKit
+ call_set_temperature = async_mock_service(hass, DOMAIN_WATER_HEATER,
+ 'set_temperature')
+
+ await hass.async_add_job(acc.char_target_temp.client_update_value, 60)
+ await hass.async_block_till_done()
+ assert call_set_temperature
+ assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id
+ assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 140.0
+ assert acc.char_target_temp.value == 60.0
+ assert len(events) == 1
+ assert events[-1].data[ATTR_VALUE] == '140.0°F'
+
+
+async def test_water_heater_get_temperature_range(hass, hk_driver, cls):
+ """Test if temperature range is evaluated correctly."""
+ entity_id = 'water_heater.test'
+
+ hass.states.async_set(entity_id, STATE_HEAT)
+ await hass.async_block_till_done()
+ acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None)
+
+ hass.states.async_set(entity_id, STATE_HEAT,
+ {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25})
+ await hass.async_block_till_done()
+ assert acc.get_temperature_range() == (20, 25)
+
+ acc._unit = TEMP_FAHRENHEIT
+ hass.states.async_set(entity_id, STATE_OFF,
+ {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70})
+ await hass.async_block_till_done()
+ assert acc.get_temperature_range() == (15.6, 21.1)
diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py
index 118cdb3c995..5a768820e18 100644
--- a/tests/components/light/test_mqtt.py
+++ b/tests/components/light/test_mqtt.py
@@ -137,6 +137,21 @@ light:
payload_on: "on"
payload_off: "off"
+Configuration for HS Version with brightness:
+
+light:
+ platform: mqtt
+ name: "Office Light HS"
+ state_topic: "office/hs1/light/status"
+ command_topic: "office/hs1/light/switch"
+ brightness_state_topic: "office/hs1/brightness/status"
+ brightness_command_topic: "office/hs1/brightness/set"
+ hs_state_topic: "office/hs1/hs/status"
+ hs_command_topic: "office/hs1/hs/set"
+ qos: 0
+ payload_on: "on"
+ payload_off: "off"
+
"""
import unittest
from unittest import mock
@@ -180,7 +195,7 @@ class TestLightMQTT(unittest.TestCase):
})
self.assertIsNone(self.hass.states.get('light.test'))
- def test_no_color_brightness_color_temp_white_xy_if_no_topics(self):
+ def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(self):
"""Test if there is no color and brightness if no topic."""
with assert_setup_component(1, light.DOMAIN):
assert setup_component(self.hass, light.DOMAIN, {
@@ -197,6 +212,7 @@ class TestLightMQTT(unittest.TestCase):
self.assertIsNone(state.attributes.get('rgb_color'))
self.assertIsNone(state.attributes.get('brightness'))
self.assertIsNone(state.attributes.get('color_temp'))
+ self.assertIsNone(state.attributes.get('hs_color'))
self.assertIsNone(state.attributes.get('white_value'))
self.assertIsNone(state.attributes.get('xy_color'))
@@ -208,6 +224,7 @@ class TestLightMQTT(unittest.TestCase):
self.assertIsNone(state.attributes.get('rgb_color'))
self.assertIsNone(state.attributes.get('brightness'))
self.assertIsNone(state.attributes.get('color_temp'))
+ self.assertIsNone(state.attributes.get('hs_color'))
self.assertIsNone(state.attributes.get('white_value'))
self.assertIsNone(state.attributes.get('xy_color'))
@@ -226,6 +243,8 @@ class TestLightMQTT(unittest.TestCase):
'color_temp_command_topic': 'test_light_rgb/color_temp/set',
'effect_state_topic': 'test_light_rgb/effect/status',
'effect_command_topic': 'test_light_rgb/effect/set',
+ 'hs_state_topic': 'test_light_rgb/hs/status',
+ 'hs_command_topic': 'test_light_rgb/hs/set',
'white_value_state_topic': 'test_light_rgb/white_value/status',
'white_value_command_topic': 'test_light_rgb/white_value/set',
'xy_state_topic': 'test_light_rgb/xy/status',
@@ -244,6 +263,7 @@ class TestLightMQTT(unittest.TestCase):
self.assertIsNone(state.attributes.get('brightness'))
self.assertIsNone(state.attributes.get('color_temp'))
self.assertIsNone(state.attributes.get('effect'))
+ self.assertIsNone(state.attributes.get('hs_color'))
self.assertIsNone(state.attributes.get('white_value'))
self.assertIsNone(state.attributes.get('xy_color'))
self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
@@ -257,6 +277,7 @@ class TestLightMQTT(unittest.TestCase):
self.assertEqual(255, state.attributes.get('brightness'))
self.assertEqual(150, state.attributes.get('color_temp'))
self.assertEqual('none', state.attributes.get('effect'))
+ self.assertEqual((0, 0), state.attributes.get('hs_color'))
self.assertEqual(255, state.attributes.get('white_value'))
self.assertEqual((0.323, 0.329), state.attributes.get('xy_color'))
@@ -309,6 +330,14 @@ class TestLightMQTT(unittest.TestCase):
self.assertEqual((255, 255, 255),
light_state.attributes.get('rgb_color'))
+ fire_mqtt_message(self.hass, 'test_light_rgb/hs/status',
+ '200,50')
+ self.hass.block_till_done()
+
+ light_state = self.hass.states.get('light.test')
+ self.assertEqual((200, 50),
+ light_state.attributes.get('hs_color'))
+
fire_mqtt_message(self.hass, 'test_light_rgb/xy/status',
'0.675,0.322')
self.hass.block_till_done()
@@ -364,6 +393,41 @@ class TestLightMQTT(unittest.TestCase):
self.assertEqual(255,
light_state.attributes['brightness'])
+ def test_brightness_from_rgb_controlling_scale(self):
+ """Test the brightness controlling scale."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt',
+ 'name': 'test',
+ 'state_topic': 'test_scale_rgb/status',
+ 'command_topic': 'test_scale_rgb/set',
+ 'rgb_state_topic': 'test_scale_rgb/rgb/status',
+ 'rgb_command_topic': 'test_scale_rgb/rgb/set',
+ 'qos': 0,
+ 'payload_on': 'on',
+ 'payload_off': 'off'
+ }
+ })
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertIsNone(state.attributes.get('brightness'))
+ self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
+
+ fire_mqtt_message(self.hass, 'test_scale_rgb/status', 'on')
+ fire_mqtt_message(self.hass, 'test_scale_rgb/rgb/status', '255,0,0')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(255, state.attributes.get('brightness'))
+
+ fire_mqtt_message(self.hass, 'test_scale_rgb/rgb/status', '127,0,0')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(127, state.attributes.get('brightness'))
+
def test_white_value_controlling_scale(self):
"""Test the white_value controlling scale."""
with assert_setup_component(1, light.DOMAIN):
@@ -412,7 +476,7 @@ class TestLightMQTT(unittest.TestCase):
light_state.attributes['white_value'])
def test_controlling_state_via_topic_with_templates(self):
- """Test the setting og the state with a template."""
+ """Test the setting of the state with a template."""
config = {light.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
@@ -422,11 +486,13 @@ class TestLightMQTT(unittest.TestCase):
'rgb_command_topic': 'test_light_rgb/rgb/set',
'color_temp_command_topic': 'test_light_rgb/color_temp/set',
'effect_command_topic': 'test_light_rgb/effect/set',
+ 'hs_command_topic': 'test_light_rgb/hs/set',
'white_value_command_topic': 'test_light_rgb/white_value/set',
'xy_command_topic': 'test_light_rgb/xy/set',
'brightness_state_topic': 'test_light_rgb/brightness/status',
'color_temp_state_topic': 'test_light_rgb/color_temp/status',
'effect_state_topic': 'test_light_rgb/effect/status',
+ 'hs_state_topic': 'test_light_rgb/hs/status',
'rgb_state_topic': 'test_light_rgb/rgb/status',
'white_value_state_topic': 'test_light_rgb/white_value/status',
'xy_state_topic': 'test_light_rgb/xy/status',
@@ -434,6 +500,7 @@ class TestLightMQTT(unittest.TestCase):
'brightness_value_template': '{{ value_json.hello }}',
'color_temp_value_template': '{{ value_json.hello }}',
'effect_value_template': '{{ value_json.hello }}',
+ 'hs_value_template': '{{ value_json.hello | join(",") }}',
'rgb_value_template': '{{ value_json.hello | join(",") }}',
'white_value_template': '{{ value_json.hello }}',
'xy_value_template': '{{ value_json.hello | join(",") }}',
@@ -459,17 +526,28 @@ class TestLightMQTT(unittest.TestCase):
'{"hello": "rainbow"}')
fire_mqtt_message(self.hass, 'test_light_rgb/white_value/status',
'{"hello": "75"}')
- fire_mqtt_message(self.hass, 'test_light_rgb/xy/status',
- '{"hello": [0.123,0.123]}')
self.hass.block_till_done()
state = self.hass.states.get('light.test')
self.assertEqual(STATE_ON, state.state)
self.assertEqual(50, state.attributes.get('brightness'))
- self.assertEqual((0, 123, 255), state.attributes.get('rgb_color'))
+ self.assertEqual((84, 169, 255), state.attributes.get('rgb_color'))
self.assertEqual(300, state.attributes.get('color_temp'))
self.assertEqual('rainbow', state.attributes.get('effect'))
self.assertEqual(75, state.attributes.get('white_value'))
+
+ fire_mqtt_message(self.hass, 'test_light_rgb/hs/status',
+ '{"hello": [100,50]}')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual((100, 50), state.attributes.get('hs_color'))
+
+ fire_mqtt_message(self.hass, 'test_light_rgb/xy/status',
+ '{"hello": [0.123,0.123]}')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
self.assertEqual((0.14, 0.131), state.attributes.get('xy_color'))
def test_sending_mqtt_commands_and_optimistic(self):
@@ -482,6 +560,7 @@ class TestLightMQTT(unittest.TestCase):
'rgb_command_topic': 'test_light_rgb/rgb/set',
'color_temp_command_topic': 'test_light_rgb/color_temp/set',
'effect_command_topic': 'test_light_rgb/effect/set',
+ 'hs_command_topic': 'test_light_rgb/hs/set',
'white_value_command_topic': 'test_light_rgb/white_value/set',
'xy_command_topic': 'test_light_rgb/xy/set',
'effect_list': ['colorloop', 'random'],
@@ -529,6 +608,8 @@ class TestLightMQTT(unittest.TestCase):
self.mock_publish.reset_mock()
common.turn_on(self.hass, 'light.test',
brightness=50, xy_color=[0.123, 0.123])
+ common.turn_on(self.hass, 'light.test',
+ brightness=50, hs_color=[359, 78])
common.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0],
white_value=80)
self.hass.block_till_done()
@@ -537,6 +618,7 @@ class TestLightMQTT(unittest.TestCase):
mock.call('test_light_rgb/set', 'on', 2, False),
mock.call('test_light_rgb/rgb/set', '255,128,0', 2, False),
mock.call('test_light_rgb/brightness/set', 50, 2, False),
+ mock.call('test_light_rgb/hs/set', '359.0,78.0', 2, False),
mock.call('test_light_rgb/white_value/set', 80, 2, False),
mock.call('test_light_rgb/xy/set', '0.14,0.131', 2, False),
], any_order=True)
@@ -545,6 +627,7 @@ class TestLightMQTT(unittest.TestCase):
self.assertEqual(STATE_ON, state.state)
self.assertEqual((255, 128, 0), state.attributes['rgb_color'])
self.assertEqual(50, state.attributes['brightness'])
+ self.assertEqual((30.118, 100), state.attributes['hs_color'])
self.assertEqual(80, state.attributes['white_value'])
self.assertEqual((0.611, 0.375), state.attributes['xy_color'])
@@ -652,6 +735,30 @@ class TestLightMQTT(unittest.TestCase):
self.assertEqual(STATE_ON, state.state)
self.assertEqual('none', state.attributes.get('effect'))
+ def test_show_hs_if_only_command_topic(self):
+ """Test the hs if only a command topic is present."""
+ config = {light.DOMAIN: {
+ 'platform': 'mqtt',
+ 'name': 'test',
+ 'hs_command_topic': 'test_light_rgb/hs/set',
+ 'command_topic': 'test_light_rgb/set',
+ 'state_topic': 'test_light_rgb/status',
+ }}
+
+ with assert_setup_component(1, light.DOMAIN):
+ assert setup_component(self.hass, light.DOMAIN, config)
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertIsNone(state.attributes.get('hs_color'))
+
+ fire_mqtt_message(self.hass, 'test_light_rgb/status', 'ON')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertEqual((0, 0), state.attributes.get('hs_color'))
+
def test_show_white_value_if_only_command_topic(self):
"""Test the white_value if only a command topic is present."""
config = {light.DOMAIN: {
@@ -822,6 +929,39 @@ class TestLightMQTT(unittest.TestCase):
mock.call('test_light/bright', 50, 0, False)
], any_order=True)
+ def test_on_command_rgb(self):
+ """Test on command in RGB brightness mode."""
+ config = {light.DOMAIN: {
+ 'platform': 'mqtt',
+ 'name': 'test',
+ 'command_topic': 'test_light/set',
+ 'rgb_command_topic': "test_light/rgb",
+ }}
+
+ with assert_setup_component(1, light.DOMAIN):
+ assert setup_component(self.hass, light.DOMAIN, config)
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+
+ common.turn_on(self.hass, 'light.test', brightness=127)
+ self.hass.block_till_done()
+
+ # Should get the following MQTT messages.
+ # test_light/rgb: '127,127,127'
+ # test_light/set: 'ON'
+ self.mock_publish.async_publish.assert_has_calls([
+ mock.call('test_light/rgb', '127,127,127', 0, False),
+ mock.call('test_light/set', 'ON', 0, False),
+ ], any_order=True)
+ self.mock_publish.async_publish.reset_mock()
+
+ common.turn_off(self.hass, 'light.test')
+ self.hass.block_till_done()
+
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light/set', 'OFF', 0, False)
+
def test_default_availability_payload(self):
"""Test availability by default payload with defined topic."""
self.assertTrue(setup_component(self.hass, light.DOMAIN, {
diff --git a/tests/components/lock/test_template.py b/tests/components/lock/test_template.py
new file mode 100644
index 00000000000..7b67a68bde1
--- /dev/null
+++ b/tests/components/lock/test_template.py
@@ -0,0 +1,309 @@
+"""The tests for the Template lock platform."""
+import logging
+
+from homeassistant.core import callback
+from homeassistant import setup
+from homeassistant.components import lock
+from homeassistant.const import STATE_ON, STATE_OFF
+
+from tests.common import (get_test_home_assistant,
+ assert_setup_component)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class TestTemplateLock:
+ """Test the Template lock."""
+
+ hass = None
+ calls = None
+ # pylint: disable=invalid-name
+
+ def setup_method(self, method):
+ """Set up things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.calls = []
+
+ @callback
+ def record_call(service):
+ """Track function calls."""
+ self.calls.append(service)
+
+ self.hass.services.register('test', 'automation', record_call)
+
+ def teardown_method(self, method):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ def test_template_state(self):
+ """Test template."""
+ with assert_setup_component(1, 'lock'):
+ assert setup.setup_component(self.hass, 'lock', {
+ 'lock': {
+ 'platform': 'template',
+ 'name': 'Test template lock',
+ 'value_template':
+ "{{ states.switch.test_state.state }}",
+ 'lock': {
+ 'service': 'switch.turn_on',
+ 'entity_id': 'switch.test_state'
+ },
+ 'unlock': {
+ 'service': 'switch.turn_off',
+ 'entity_id': 'switch.test_state'
+ }
+ }
+ })
+
+ self.hass.start()
+ self.hass.block_till_done()
+
+ self.hass.states.set('switch.test_state', STATE_ON)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('lock.test_template_lock')
+ assert state.state == lock.STATE_LOCKED
+
+ self.hass.states.set('switch.test_state', STATE_OFF)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('lock.test_template_lock')
+ assert state.state == lock.STATE_UNLOCKED
+
+ def test_template_state_boolean_on(self):
+ """Test the setting of the state with boolean on."""
+ with assert_setup_component(1, 'lock'):
+ assert setup.setup_component(self.hass, 'lock', {
+ 'lock': {
+ 'platform': 'template',
+ 'value_template':
+ "{{ 1 == 1 }}",
+ 'lock': {
+ 'service': 'switch.turn_on',
+ 'entity_id': 'switch.test_state'
+ },
+ 'unlock': {
+ 'service': 'switch.turn_off',
+ 'entity_id': 'switch.test_state'
+ }
+ }
+ })
+
+ self.hass.start()
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('lock.template_lock')
+ assert state.state == lock.STATE_LOCKED
+
+ def test_template_state_boolean_off(self):
+ """Test the setting of the state with off."""
+ with assert_setup_component(1, 'lock'):
+ assert setup.setup_component(self.hass, 'lock', {
+ 'lock': {
+ 'platform': 'template',
+ 'value_template':
+ "{{ 1 == 2 }}",
+ 'lock': {
+ 'service': 'switch.turn_on',
+ 'entity_id': 'switch.test_state'
+ },
+ 'unlock': {
+ 'service': 'switch.turn_off',
+ 'entity_id': 'switch.test_state'
+ }
+ }
+ })
+
+ self.hass.start()
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('lock.template_lock')
+ assert state.state == lock.STATE_UNLOCKED
+
+ def test_template_syntax_error(self):
+ """Test templating syntax error."""
+ with assert_setup_component(0, 'lock'):
+ assert setup.setup_component(self.hass, 'lock', {
+ 'lock': {
+ 'platform': 'template',
+ 'value_template':
+ "{% if rubbish %}",
+ 'lock': {
+ 'service': 'switch.turn_on',
+ 'entity_id': 'switch.test_state'
+ },
+ 'unlock': {
+ 'service': 'switch.turn_off',
+ 'entity_id': 'switch.test_state'
+ }
+ }
+ })
+
+ self.hass.start()
+ self.hass.block_till_done()
+
+ assert self.hass.states.all() == []
+
+ def test_invalid_name_does_not_create(self):
+ """Test invalid name."""
+ with assert_setup_component(0, 'lock'):
+ assert setup.setup_component(self.hass, 'lock', {
+ 'switch': {
+ 'platform': 'lock',
+ 'name': '{{%}',
+ 'value_template':
+ "{{ rubbish }",
+ 'lock': {
+ 'service': 'switch.turn_on',
+ 'entity_id': 'switch.test_state'
+ },
+ 'unlock': {
+ 'service': 'switch.turn_off',
+ 'entity_id': 'switch.test_state'
+ }
+ }
+ })
+
+ self.hass.start()
+ self.hass.block_till_done()
+
+ assert self.hass.states.all() == []
+
+ def test_invalid_lock_does_not_create(self):
+ """Test invalid lock."""
+ with assert_setup_component(0, 'lock'):
+ assert setup.setup_component(self.hass, 'lock', {
+ 'lock': {
+ 'platform': 'template',
+ 'value_template': "Invalid"
+ }
+ })
+
+ self.hass.start()
+ self.hass.block_till_done()
+
+ assert self.hass.states.all() == []
+
+ def test_missing_template_does_not_create(self):
+ """Test missing template."""
+ with assert_setup_component(0, 'lock'):
+ assert setup.setup_component(self.hass, 'lock', {
+ 'lock': {
+ 'platform': 'template',
+ 'not_value_template':
+ "{{ states.switch.test_state.state }}",
+ 'lock': {
+ 'service': 'switch.turn_on',
+ 'entity_id': 'switch.test_state'
+ },
+ 'unlock': {
+ 'service': 'switch.turn_off',
+ 'entity_id': 'switch.test_state'
+ }
+ }
+ })
+
+ self.hass.start()
+ self.hass.block_till_done()
+
+ assert self.hass.states.all() == []
+
+ def test_no_template_match_all(self, caplog):
+ """Test that we do not allow locks that match on all."""
+ with assert_setup_component(1, 'lock'):
+ assert setup.setup_component(self.hass, 'lock', {
+ 'lock': {
+ 'platform': 'template',
+ 'value_template': '{{ 1 + 1 }}',
+ 'lock': {
+ 'service': 'switch.turn_on',
+ 'entity_id': 'switch.test_state'
+ },
+ 'unlock': {
+ 'service': 'switch.turn_off',
+ 'entity_id': 'switch.test_state'
+ }
+ }
+ })
+
+ self.hass.start()
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('lock.template_lock')
+ assert state.state == lock.STATE_UNLOCKED
+
+ assert ('Template lock Template Lock has no entity ids configured '
+ 'to track nor were we able to extract the entities to track '
+ 'from the value_template template. This entity will only '
+ 'be able to be updated manually.') in caplog.text
+
+ self.hass.states.set('lock.template_lock', lock.STATE_LOCKED)
+ self.hass.block_till_done()
+ state = self.hass.states.get('lock.template_lock')
+ assert state.state == lock.STATE_LOCKED
+
+ def test_lock_action(self):
+ """Test lock action."""
+ assert setup.setup_component(self.hass, 'lock', {
+ 'lock': {
+ 'platform': 'template',
+ 'value_template':
+ "{{ states.switch.test_state.state }}",
+ 'lock': {
+ 'service': 'test.automation'
+ },
+ 'unlock': {
+ 'service': 'switch.turn_off',
+ 'entity_id': 'switch.test_state'
+ }
+ }
+ })
+
+ self.hass.start()
+ self.hass.block_till_done()
+
+ self.hass.states.set('switch.test_state', STATE_OFF)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('lock.template_lock')
+ assert state.state == lock.STATE_UNLOCKED
+
+ self.hass.services.call(lock.DOMAIN, lock.SERVICE_LOCK, {
+ lock.ATTR_ENTITY_ID: 'lock.template_lock'
+ })
+ self.hass.block_till_done()
+
+ assert len(self.calls) == 1
+
+ def test_unlock_action(self):
+ """Test unlock action."""
+ assert setup.setup_component(self.hass, 'lock', {
+ 'lock': {
+ 'platform': 'template',
+ 'value_template':
+ "{{ states.switch.test_state.state }}",
+ 'lock': {
+ 'service': 'switch.turn_on',
+ 'entity_id': 'switch.test_state'
+ },
+ 'unlock': {
+ 'service': 'test.automation'
+ }
+ }
+ })
+
+ self.hass.start()
+ self.hass.block_till_done()
+
+ self.hass.states.set('switch.test_state', STATE_ON)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('lock.template_lock')
+ assert state.state == lock.STATE_LOCKED
+
+ self.hass.services.call(lock.DOMAIN, lock.SERVICE_UNLOCK, {
+ lock.ATTR_ENTITY_ID: 'lock.template_lock'
+ })
+ self.hass.block_till_done()
+
+ assert len(self.calls) == 1
diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py
index 0fde6de902c..1ce0f9ff602 100644
--- a/tests/components/lovelace/test_init.py
+++ b/tests/components/lovelace/test_init.py
@@ -1,9 +1,178 @@
"""Test the Lovelace initialization."""
+import os
+import unittest
from unittest.mock import patch
+from tempfile import mkdtemp
+from ruamel.yaml import YAML
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
from homeassistant.components.websocket_api.const import TYPE_RESULT
+from homeassistant.components.lovelace import (load_yaml,
+ save_yaml, load_config,
+ UnsupportedYamlError)
+
+TEST_YAML_A = """\
+title: My Awesome Home
+# Include external resources
+resources:
+ - url: /local/my-custom-card.js
+ type: js
+ - url: /local/my-webfont.css
+ type: css
+
+# Exclude entities from "Unused entities" view
+excluded_entities:
+ - weblink.router
+views:
+ # View tab title.
+ - title: Example
+ # Optional unique id for direct access /lovelace/${id}
+ id: example
+ # Optional background (overwrites the global background).
+ background: radial-gradient(crimson, skyblue)
+ # Each view can have a different theme applied.
+ theme: dark-mode
+ # The cards to show on this view.
+ cards:
+ # The filter card will filter entities for their state
+ - type: entity-filter
+ entities:
+ - device_tracker.paulus
+ - device_tracker.anne_there
+ state_filter:
+ - 'home'
+ card:
+ type: glance
+ title: People that are home
+
+ # The picture entity card will represent an entity with a picture
+ - type: picture-entity
+ image: https://www.home-assistant.io/images/default-social.png
+ entity: light.bed_light
+
+ # Specify a tab icon if you want the view tab to be an icon.
+ - icon: mdi:home-assistant
+ # Title of the view. Will be used as the tooltip for tab icon
+ title: Second view
+ cards:
+ - id: test
+ type: entities
+ # Entities card will take a list of entities and show their state.
+ - type: entities
+ # Title of the entities card
+ title: Example
+ # The entities here will be shown in the same order as specified.
+ # Each entry is an entity ID or a map with extra options.
+ entities:
+ - light.kitchen
+ - switch.ac
+ - entity: light.living_room
+ # Override the name to use
+ name: LR Lights
+
+ # The markdown card will render markdown text.
+ - type: markdown
+ title: Lovelace
+ content: >
+ Welcome to your **Lovelace UI**.
+"""
+
+TEST_YAML_B = """\
+title: Home
+views:
+ - title: Dashboard
+ id: dashboard
+ icon: mdi:home
+ cards:
+ - id: testid
+ type: vertical-stack
+ cards:
+ - type: picture-entity
+ entity: group.sample
+ name: Sample
+ image: /local/images/sample.jpg
+ tap_action: toggle
+"""
+
+# Test data that can not be loaded as YAML
+TEST_BAD_YAML = """\
+title: Home
+views:
+ - title: Dashboard
+ icon: mdi:home
+ cards:
+ - id: testid
+ type: vertical-stack
+"""
+
+# Test unsupported YAML
+TEST_UNSUP_YAML = """\
+title: Home
+views:
+ - title: Dashboard
+ icon: mdi:home
+ cards: !include cards.yaml
+"""
+
+
+class TestYAML(unittest.TestCase):
+ """Test lovelace.yaml save and load."""
+
+ def setUp(self):
+ """Set up for tests."""
+ self.tmp_dir = mkdtemp()
+ self.yaml = YAML(typ='rt')
+
+ def tearDown(self):
+ """Clean up after tests."""
+ for fname in os.listdir(self.tmp_dir):
+ os.remove(os.path.join(self.tmp_dir, fname))
+ os.rmdir(self.tmp_dir)
+
+ def _path_for(self, leaf_name):
+ return os.path.join(self.tmp_dir, leaf_name+".yaml")
+
+ def test_save_and_load(self):
+ """Test saving and loading back."""
+ fname = self._path_for("test1")
+ save_yaml(fname, self.yaml.load(TEST_YAML_A))
+ data = load_yaml(fname)
+ self.assertEqual(data, self.yaml.load(TEST_YAML_A))
+
+ def test_overwrite_and_reload(self):
+ """Test that we can overwrite an existing file and read back."""
+ fname = self._path_for("test3")
+ save_yaml(fname, self.yaml.load(TEST_YAML_A))
+ save_yaml(fname, self.yaml.load(TEST_YAML_B))
+ data = load_yaml(fname)
+ self.assertEqual(data, self.yaml.load(TEST_YAML_B))
+
+ def test_load_bad_data(self):
+ """Test error from trying to load unserialisable data."""
+ fname = self._path_for("test5")
+ with open(fname, "w") as fh:
+ fh.write(TEST_BAD_YAML)
+ with self.assertRaises(HomeAssistantError):
+ load_yaml(fname)
+
+ def test_add_id(self):
+ """Test if id is added."""
+ fname = self._path_for("test6")
+ with patch('homeassistant.components.lovelace.load_yaml',
+ return_value=self.yaml.load(TEST_YAML_A)), \
+ patch('homeassistant.components.lovelace.save_yaml'):
+ data = load_config(fname)
+ assert 'id' in data['views'][0]['cards'][0]
+ assert 'id' in data['views'][1]
+
+ def test_id_not_changed(self):
+ """Test if id is not changed if already exists."""
+ fname = self._path_for("test7")
+ with patch('homeassistant.components.lovelace.load_yaml',
+ return_value=self.yaml.load(TEST_YAML_B)):
+ data = load_config(fname)
+ self.assertEqual(data, self.yaml.load(TEST_YAML_B))
async def test_deprecated_lovelace_ui(hass, hass_ws_client):
@@ -11,7 +180,7 @@ async def test_deprecated_lovelace_ui(hass, hass_ws_client):
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
- with patch('homeassistant.components.lovelace.load_yaml',
+ with patch('homeassistant.components.lovelace.load_config',
return_value={'hello': 'world'}):
await client.send_json({
'id': 5,
@@ -30,7 +199,7 @@ async def test_deprecated_lovelace_ui_not_found(hass, hass_ws_client):
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
- with patch('homeassistant.components.lovelace.load_yaml',
+ with patch('homeassistant.components.lovelace.load_config',
side_effect=FileNotFoundError):
await client.send_json({
'id': 5,
@@ -49,7 +218,7 @@ async def test_deprecated_lovelace_ui_load_err(hass, hass_ws_client):
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
- with patch('homeassistant.components.lovelace.load_yaml',
+ with patch('homeassistant.components.lovelace.load_config',
side_effect=HomeAssistantError):
await client.send_json({
'id': 5,
@@ -68,7 +237,7 @@ async def test_lovelace_ui(hass, hass_ws_client):
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
- with patch('homeassistant.components.lovelace.load_yaml',
+ with patch('homeassistant.components.lovelace.load_config',
return_value={'hello': 'world'}):
await client.send_json({
'id': 5,
@@ -87,7 +256,7 @@ async def test_lovelace_ui_not_found(hass, hass_ws_client):
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
- with patch('homeassistant.components.lovelace.load_yaml',
+ with patch('homeassistant.components.lovelace.load_config',
side_effect=FileNotFoundError):
await client.send_json({
'id': 5,
@@ -102,11 +271,11 @@ async def test_lovelace_ui_not_found(hass, hass_ws_client):
async def test_lovelace_ui_load_err(hass, hass_ws_client):
- """Test lovelace_ui command cannot find file."""
+ """Test lovelace_ui command load error."""
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
- with patch('homeassistant.components.lovelace.load_yaml',
+ with patch('homeassistant.components.lovelace.load_config',
side_effect=HomeAssistantError):
await client.send_json({
'id': 5,
@@ -118,3 +287,209 @@ async def test_lovelace_ui_load_err(hass, hass_ws_client):
assert msg['type'] == TYPE_RESULT
assert msg['success'] is False
assert msg['error']['code'] == 'load_error'
+
+
+async def test_lovelace_ui_load_json_err(hass, hass_ws_client):
+ """Test lovelace_ui command load error."""
+ await async_setup_component(hass, 'lovelace')
+ client = await hass_ws_client(hass)
+
+ with patch('homeassistant.components.lovelace.load_config',
+ side_effect=UnsupportedYamlError):
+ await client.send_json({
+ 'id': 5,
+ 'type': 'lovelace/config',
+ })
+ msg = await client.receive_json()
+
+ assert msg['id'] == 5
+ assert msg['type'] == TYPE_RESULT
+ assert msg['success'] is False
+ assert msg['error']['code'] == 'unsupported_error'
+
+
+async def test_lovelace_get_card(hass, hass_ws_client):
+ """Test get_card command."""
+ await async_setup_component(hass, 'lovelace')
+ client = await hass_ws_client(hass)
+ yaml = YAML(typ='rt')
+
+ with patch('homeassistant.components.lovelace.load_yaml',
+ return_value=yaml.load(TEST_YAML_A)):
+ await client.send_json({
+ 'id': 5,
+ 'type': 'lovelace/config/card/get',
+ 'card_id': 'test',
+ })
+ msg = await client.receive_json()
+
+ assert msg['id'] == 5
+ assert msg['type'] == TYPE_RESULT
+ assert msg['success']
+ assert msg['result'] == 'id: test\ntype: entities\n'
+
+
+async def test_lovelace_get_card_not_found(hass, hass_ws_client):
+ """Test get_card command cannot find card."""
+ await async_setup_component(hass, 'lovelace')
+ client = await hass_ws_client(hass)
+ yaml = YAML(typ='rt')
+
+ with patch('homeassistant.components.lovelace.load_yaml',
+ return_value=yaml.load(TEST_YAML_A)):
+ await client.send_json({
+ 'id': 5,
+ 'type': 'lovelace/config/card/get',
+ 'card_id': 'not_found',
+ })
+ msg = await client.receive_json()
+
+ assert msg['id'] == 5
+ assert msg['type'] == TYPE_RESULT
+ assert msg['success'] is False
+ assert msg['error']['code'] == 'card_not_found'
+
+
+async def test_lovelace_get_card_bad_yaml(hass, hass_ws_client):
+ """Test get_card command bad yaml."""
+ await async_setup_component(hass, 'lovelace')
+ client = await hass_ws_client(hass)
+
+ with patch('homeassistant.components.lovelace.load_yaml',
+ side_effect=HomeAssistantError):
+ await client.send_json({
+ 'id': 5,
+ 'type': 'lovelace/config/card/get',
+ 'card_id': 'testid',
+ })
+ msg = await client.receive_json()
+
+ assert msg['id'] == 5
+ assert msg['type'] == TYPE_RESULT
+ assert msg['success'] is False
+ assert msg['error']['code'] == 'load_error'
+
+
+async def test_lovelace_update_card(hass, hass_ws_client):
+ """Test update_card command."""
+ await async_setup_component(hass, 'lovelace')
+ client = await hass_ws_client(hass)
+ yaml = YAML(typ='rt')
+
+ with patch('homeassistant.components.lovelace.load_yaml',
+ return_value=yaml.load(TEST_YAML_A)), \
+ patch('homeassistant.components.lovelace.save_yaml') \
+ as save_yaml_mock:
+ await client.send_json({
+ 'id': 5,
+ 'type': 'lovelace/config/card/update',
+ 'card_id': 'test',
+ 'card_config': 'id: test\ntype: glance\n',
+ })
+ msg = await client.receive_json()
+
+ result = save_yaml_mock.call_args_list[0][0][1]
+ assert result.mlget(['views', 1, 'cards', 0, 'type'],
+ list_ok=True) == 'glance'
+ assert msg['id'] == 5
+ assert msg['type'] == TYPE_RESULT
+ assert msg['success']
+
+
+async def test_lovelace_update_card_not_found(hass, hass_ws_client):
+ """Test update_card command cannot find card."""
+ await async_setup_component(hass, 'lovelace')
+ client = await hass_ws_client(hass)
+ yaml = YAML(typ='rt')
+
+ with patch('homeassistant.components.lovelace.load_yaml',
+ return_value=yaml.load(TEST_YAML_A)):
+ await client.send_json({
+ 'id': 5,
+ 'type': 'lovelace/config/card/update',
+ 'card_id': 'not_found',
+ 'card_config': 'id: test\ntype: glance\n',
+ })
+ msg = await client.receive_json()
+
+ assert msg['id'] == 5
+ assert msg['type'] == TYPE_RESULT
+ assert msg['success'] is False
+ assert msg['error']['code'] == 'card_not_found'
+
+
+async def test_lovelace_update_card_bad_yaml(hass, hass_ws_client):
+ """Test update_card command bad yaml."""
+ await async_setup_component(hass, 'lovelace')
+ client = await hass_ws_client(hass)
+ yaml = YAML(typ='rt')
+
+ with patch('homeassistant.components.lovelace.load_yaml',
+ return_value=yaml.load(TEST_YAML_A)), \
+ patch('homeassistant.components.lovelace.yaml_to_object',
+ side_effect=HomeAssistantError):
+ await client.send_json({
+ 'id': 5,
+ 'type': 'lovelace/config/card/update',
+ 'card_id': 'test',
+ 'card_config': 'id: test\ntype: glance\n',
+ })
+ msg = await client.receive_json()
+
+ assert msg['id'] == 5
+ assert msg['type'] == TYPE_RESULT
+ assert msg['success'] is False
+ assert msg['error']['code'] == 'save_error'
+
+
+async def test_lovelace_add_card(hass, hass_ws_client):
+ """Test add_card command."""
+ await async_setup_component(hass, 'lovelace')
+ client = await hass_ws_client(hass)
+ yaml = YAML(typ='rt')
+
+ with patch('homeassistant.components.lovelace.load_yaml',
+ return_value=yaml.load(TEST_YAML_A)), \
+ patch('homeassistant.components.lovelace.save_yaml') \
+ as save_yaml_mock:
+ await client.send_json({
+ 'id': 5,
+ 'type': 'lovelace/config/card/add',
+ 'view_id': 'example',
+ 'card_config': 'id: test\ntype: added\n',
+ })
+ msg = await client.receive_json()
+
+ result = save_yaml_mock.call_args_list[0][0][1]
+ assert result.mlget(['views', 0, 'cards', 2, 'type'],
+ list_ok=True) == 'added'
+ assert msg['id'] == 5
+ assert msg['type'] == TYPE_RESULT
+ assert msg['success']
+
+
+async def test_lovelace_add_card_position(hass, hass_ws_client):
+ """Test add_card command."""
+ await async_setup_component(hass, 'lovelace')
+ client = await hass_ws_client(hass)
+ yaml = YAML(typ='rt')
+
+ with patch('homeassistant.components.lovelace.load_yaml',
+ return_value=yaml.load(TEST_YAML_A)), \
+ patch('homeassistant.components.lovelace.save_yaml') \
+ as save_yaml_mock:
+ await client.send_json({
+ 'id': 5,
+ 'type': 'lovelace/config/card/add',
+ 'view_id': 'example',
+ 'position': 0,
+ 'card_config': 'id: test\ntype: added\n',
+ })
+ msg = await client.receive_json()
+
+ result = save_yaml_mock.call_args_list[0][0][1]
+ assert result.mlget(['views', 0, 'cards', 0, 'type'],
+ list_ok=True) == 'added'
+ assert msg['id'] == 5
+ assert msg['type'] == TYPE_RESULT
+ assert msg['success']
diff --git a/tests/components/media_player/test_samsungtv.py b/tests/components/media_player/test_samsungtv.py
index 5551a86df05..dda6562af77 100644
--- a/tests/components/media_player/test_samsungtv.py
+++ b/tests/components/media_player/test_samsungtv.py
@@ -2,7 +2,6 @@
import asyncio
import unittest
from unittest.mock import call, patch, MagicMock
-from subprocess import CalledProcessError
from asynctest import mock
@@ -13,8 +12,8 @@ from homeassistant.components.media_player import SUPPORT_TURN_ON, \
MEDIA_TYPE_CHANNEL, MEDIA_TYPE_URL
from homeassistant.components.media_player.samsungtv import setup_platform, \
CONF_TIMEOUT, SamsungTVDevice, SUPPORT_SAMSUNGTV
-from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_ON, \
- CONF_MAC, STATE_OFF
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_MAC, \
+ STATE_OFF
from tests.common import MockDependency
from homeassistant.util import dt as dt_util
from datetime import timedelta
@@ -100,55 +99,24 @@ class TestSamsungTv(unittest.TestCase):
mocked_warn.assert_called_once_with("Cannot determine device")
add_entities.assert_not_called()
- @mock.patch(
- 'homeassistant.components.media_player.samsungtv.subprocess.Popen'
- )
- def test_update_on(self, mocked_popen):
+ def test_update_on(self):
"""Testing update tv on."""
- ping = mock.Mock()
- mocked_popen.return_value = ping
- ping.returncode = 0
self.device.update()
- self.assertEqual(STATE_ON, self.device._state)
+ self.assertEqual(None, self.device._state)
- @mock.patch(
- 'homeassistant.components.media_player.samsungtv.subprocess.Popen'
- )
- def test_update_off(self, mocked_popen):
+ def test_update_off(self):
"""Testing update tv off."""
- ping = mock.Mock()
- mocked_popen.return_value = ping
- ping.returncode = 1
+ _remote = mock.Mock()
+ _remote.control = mock.Mock(
+ side_effect=OSError('Boom'))
+ self.device.get_remote = mock.Mock(return_value=_remote)
self.device.update()
self.assertEqual(STATE_OFF, self.device._state)
- ping = mock.Mock()
- ping.communicate = mock.Mock(
- side_effect=CalledProcessError("BOOM", None))
- mocked_popen.return_value = ping
- self.device.update()
- self.assertEqual(STATE_OFF, self.device._state)
-
- @mock.patch(
- 'homeassistant.components.media_player.samsungtv.subprocess.Popen'
- )
- def test_timeout(self, mocked_popen):
- """Test timeout use."""
- ping = mock.Mock()
- mocked_popen.return_value = ping
- ping.returncode = 0
- self.device.update()
- expected_timeout = self.device._config['timeout']
- timeout_arg = '-W{}'.format(expected_timeout)
- ping_command = [
- 'ping', '-n', '-q', '-c3', timeout_arg, 'fake']
- expected_call = call(ping_command, stderr=-3, stdout=-1)
- self.assertEqual(mocked_popen.call_args, expected_call)
- self.assertEqual(STATE_ON, self.device._state)
def test_send_key(self):
"""Test for send key."""
self.device.send_key('KEY_POWER')
- self.assertEqual(STATE_ON, self.device._state)
+ self.assertEqual(None, self.device._state)
def test_send_key_broken_pipe(self):
"""Testing broken pipe Exception."""
@@ -158,7 +126,7 @@ class TestSamsungTv(unittest.TestCase):
self.device.get_remote = mock.Mock(return_value=_remote)
self.device.send_key('HELLO')
self.assertIsNone(self.device._remote)
- self.assertEqual(STATE_ON, self.device._state)
+ self.assertEqual(None, self.device._state)
def test_send_key_connection_closed_retry_succeed(self):
"""Test retry on connection closed."""
@@ -169,7 +137,7 @@ class TestSamsungTv(unittest.TestCase):
self.device.get_remote = mock.Mock(return_value=_remote)
command = 'HELLO'
self.device.send_key(command)
- self.assertEqual(STATE_ON, self.device._state)
+ self.assertEqual(None, self.device._state)
# verify that _remote.control() get called twice because of retry logic
expected = [mock.call(command),
mock.call(command)]
@@ -184,7 +152,7 @@ class TestSamsungTv(unittest.TestCase):
self.device.get_remote = mock.Mock(return_value=_remote)
self.device.send_key('HELLO')
self.assertIsNone(self.device._remote)
- self.assertEqual(STATE_ON, self.device._state)
+ self.assertEqual(None, self.device._state)
def test_send_key_os_error(self):
"""Testing broken pipe Exception."""
@@ -209,8 +177,8 @@ class TestSamsungTv(unittest.TestCase):
def test_state(self):
"""Test for state property."""
- self.device._state = STATE_ON
- self.assertEqual(STATE_ON, self.device.state)
+ self.device._state = None
+ self.assertEqual(None, self.device.state)
self.device._state = STATE_OFF
self.assertEqual(STATE_OFF, self.device.state)
diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py
index 36b022de7a6..dd3ab2e6f7a 100644
--- a/tests/components/mqtt/test_discovery.py
+++ b/tests/components/mqtt/test_discovery.py
@@ -5,6 +5,7 @@ from unittest.mock import patch
from homeassistant.components import mqtt
from homeassistant.components.mqtt.discovery import async_start, \
ALREADY_DISCOVERED
+from homeassistant.const import STATE_ON, STATE_OFF
from tests.common import async_fire_mqtt_message, mock_coro, MockConfigEntry
@@ -208,3 +209,36 @@ def test_non_duplicate_discovery(hass, mqtt_mock, caplog):
assert state_duplicate is None
assert 'Component has already been discovered: ' \
'binary_sensor bla' in caplog.text
+
+
+@asyncio.coroutine
+def test_discovery_expansion(hass, mqtt_mock, caplog):
+ """Test expansion of abbreviated discovery payload."""
+ entry = MockConfigEntry(domain=mqtt.DOMAIN)
+
+ yield from async_start(hass, 'homeassistant', {}, entry)
+
+ data = (
+ '{ "~": "some/base/topic",'
+ ' "name": "DiscoveryExpansionTest1",'
+ ' "stat_t": "test_topic/~",'
+ ' "cmd_t": "~/test_topic" }'
+ )
+
+ async_fire_mqtt_message(
+ hass, 'homeassistant/switch/bla/config', data)
+ yield from hass.async_block_till_done()
+
+ state = hass.states.get('switch.DiscoveryExpansionTest1')
+ assert state is not None
+ assert state.name == 'DiscoveryExpansionTest1'
+ assert ('switch', 'bla') in hass.data[ALREADY_DISCOVERED]
+ assert state.state == STATE_OFF
+
+ async_fire_mqtt_message(hass, 'test_topic/some/base/topic',
+ 'ON')
+ yield from hass.async_block_till_done()
+ yield from hass.async_block_till_done()
+
+ state = hass.states.get('switch.DiscoveryExpansionTest1')
+ assert state.state == STATE_ON
diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py
index 831bcaa1d24..045a411a271 100644
--- a/tests/components/mqtt/test_init.py
+++ b/tests/components/mqtt/test_init.py
@@ -190,6 +190,47 @@ class TestMQTTComponent(unittest.TestCase):
# Topic names beginning with $ SHOULD NOT be used, but can
mqtt.valid_publish_topic('$SYS/')
+ def test_entity_device_info_schema(self):
+ """Test MQTT entity device info validation."""
+ # just identifier
+ mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({
+ 'identifiers': ['abcd']
+ })
+ mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({
+ 'identifiers': 'abcd'
+ })
+ # just connection
+ mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({
+ 'connections': [
+ ['mac', '02:5b:26:a8:dc:12'],
+ ]
+ })
+ # full device info
+ mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({
+ 'identifiers': ['helloworld', 'hello'],
+ 'connections': [
+ ["mac", "02:5b:26:a8:dc:12"],
+ ["zigbee", "zigbee_id"],
+ ],
+ 'manufacturer': 'Whatever',
+ 'name': 'Beer',
+ 'model': 'Glass',
+ 'sw_version': '0.1-beta',
+ })
+ # no identifiers
+ self.assertRaises(vol.Invalid, mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, {
+ 'manufacturer': 'Whatever',
+ 'name': 'Beer',
+ 'model': 'Glass',
+ 'sw_version': '0.1-beta',
+ })
+ # empty identifiers
+ self.assertRaises(vol.Invalid, mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, {
+ 'identifiers': [],
+ 'connections': [],
+ 'name': 'Beer',
+ })
+
# pylint: disable=invalid-name
class TestMQTTCallbacks(unittest.TestCase):
diff --git a/tests/components/notify/test_homematic.py b/tests/components/notify/test_homematic.py
new file mode 100644
index 00000000000..2ea98fc020b
--- /dev/null
+++ b/tests/components/notify/test_homematic.py
@@ -0,0 +1,78 @@
+"""The tests for the Homematic notification platform."""
+
+import unittest
+
+from homeassistant.setup import setup_component
+import homeassistant.components.notify as notify_comp
+from tests.common import assert_setup_component, get_test_home_assistant
+
+
+class TestHomematicNotify(unittest.TestCase):
+ """Test the Homematic notifications."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Set up things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """Stop down everything that was started."""
+ self.hass.stop()
+
+ def test_setup_full(self):
+ """Test valid configuration."""
+ setup_component(self.hass, 'homematic', {
+ 'homematic': {
+ 'hosts': {
+ 'ccu2': {
+ 'host': '127.0.0.1'
+ }
+ }
+ }
+ })
+ with assert_setup_component(1) as handle_config:
+ assert setup_component(self.hass, 'notify', {
+ 'notify': {
+ 'name': 'test',
+ 'platform': 'homematic',
+ 'address': 'NEQXXXXXXX',
+ 'channel': 2,
+ 'param': 'SUBMIT',
+ 'value': '1,1,108000,2',
+ 'interface': 'my-interface'}
+ })
+ assert handle_config[notify_comp.DOMAIN]
+
+ def test_setup_without_optional(self):
+ """Test valid configuration without optional."""
+ setup_component(self.hass, 'homematic', {
+ 'homematic': {
+ 'hosts': {
+ 'ccu2': {
+ 'host': '127.0.0.1'
+ }
+ }
+ }
+ })
+ with assert_setup_component(1) as handle_config:
+ assert setup_component(self.hass, 'notify', {
+ 'notify': {
+ 'name': 'test',
+ 'platform': 'homematic',
+ 'address': 'NEQXXXXXXX',
+ 'channel': 2,
+ 'param': 'SUBMIT',
+ 'value': '1,1,108000,2'}
+ })
+ assert handle_config[notify_comp.DOMAIN]
+
+ def test_bad_config(self):
+ """Test invalid configuration."""
+ config = {
+ notify_comp.DOMAIN: {
+ 'name': 'test',
+ 'platform': 'homematic'
+ }
+ }
+ with assert_setup_component(0) as handle_config:
+ assert setup_component(self.hass, notify_comp.DOMAIN, config)
+ assert not handle_config[notify_comp.DOMAIN]
diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py
index 07946fbbc09..60aa990333f 100644
--- a/tests/components/openuv/test_config_flow.py
+++ b/tests/components/openuv/test_config_flow.py
@@ -1,10 +1,12 @@
"""Define tests for the OpenUV config flow."""
+from datetime import timedelta
from unittest.mock import patch
from homeassistant import data_entry_flow
from homeassistant.components.openuv import DOMAIN, config_flow
from homeassistant.const import (
- CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE)
+ CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE,
+ CONF_SCAN_INTERVAL)
from tests.common import MockConfigEntry, mock_coro
@@ -23,7 +25,7 @@ async def test_duplicate_error(hass):
flow.hass = hass
result = await flow.async_step_user(user_input=conf)
- assert result['errors'] == {'base': 'identifier_exists'}
+ assert result['errors'] == {CONF_LATITUDE: 'identifier_exists'}
async def test_invalid_api_key(hass):
@@ -41,7 +43,7 @@ async def test_invalid_api_key(hass):
with patch('pyopenuv.util.validate_api_key',
return_value=mock_coro(False)):
result = await flow.async_step_user(user_input=conf)
- assert result['errors'] == {'base': 'invalid_api_key'}
+ assert result['errors'] == {CONF_API_KEY: 'invalid_api_key'}
async def test_show_form(hass):
@@ -57,25 +59,6 @@ async def test_show_form(hass):
async def test_step_import(hass):
"""Test that the import step works."""
- conf = {
- CONF_API_KEY: '12345abcde',
- }
-
- flow = config_flow.OpenUvFlowHandler()
- flow.hass = hass
-
- with patch('pyopenuv.util.validate_api_key',
- return_value=mock_coro(True)):
- result = await flow.async_step_import(import_config=conf)
-
- assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result['title'] == '{0}, {1}'.format(
- hass.config.latitude, hass.config.longitude)
- assert result['data'] == conf
-
-
-async def test_step_user(hass):
- """Test that the user step works."""
conf = {
CONF_API_KEY: '12345abcde',
CONF_ELEVATION: 59.1234,
@@ -86,11 +69,44 @@ async def test_step_user(hass):
flow = config_flow.OpenUvFlowHandler()
flow.hass = hass
+ with patch('pyopenuv.util.validate_api_key',
+ return_value=mock_coro(True)):
+ result = await flow.async_step_import(import_config=conf)
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result['title'] == '39.128712, -104.9812612'
+ assert result['data'] == {
+ CONF_API_KEY: '12345abcde',
+ CONF_ELEVATION: 59.1234,
+ CONF_LATITUDE: 39.128712,
+ CONF_LONGITUDE: -104.9812612,
+ CONF_SCAN_INTERVAL: 1800,
+ }
+
+
+async def test_step_user(hass):
+ """Test that the user step works."""
+ conf = {
+ CONF_API_KEY: '12345abcde',
+ CONF_ELEVATION: 59.1234,
+ CONF_LATITUDE: 39.128712,
+ CONF_LONGITUDE: -104.9812612,
+ CONF_SCAN_INTERVAL: timedelta(minutes=5)
+ }
+
+ flow = config_flow.OpenUvFlowHandler()
+ flow.hass = hass
+
with patch('pyopenuv.util.validate_api_key',
return_value=mock_coro(True)):
result = await flow.async_step_user(user_input=conf)
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
- assert result['title'] == '{0}, {1}'.format(
- conf[CONF_LATITUDE], conf[CONF_LONGITUDE])
- assert result['data'] == conf
+ assert result['title'] == '39.128712, -104.9812612'
+ assert result['data'] == {
+ CONF_API_KEY: '12345abcde',
+ CONF_ELEVATION: 59.1234,
+ CONF_LATITUDE: 39.128712,
+ CONF_LONGITUDE: -104.9812612,
+ CONF_SCAN_INTERVAL: 300,
+ }
diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py
index b0683b04aa0..433d1aa2512 100644
--- a/tests/components/sensor/test_filter.py
+++ b/tests/components/sensor/test_filter.py
@@ -149,6 +149,23 @@ class TestFilterSensor(unittest.TestCase):
else:
self.assertEqual(unf, filtered.state)
+ def test_range_zero(self):
+ """Test if range filter works with zeroes as bounds."""
+ lower = 0
+ upper = 0
+ filt = RangeFilter(entity=None,
+ lower_bound=lower,
+ upper_bound=upper)
+ for unf_state in self.values:
+ unf = float(unf_state.state)
+ filtered = filt.filter_state(unf_state)
+ if unf < lower:
+ self.assertEqual(lower, filtered.state)
+ elif unf > upper:
+ self.assertEqual(upper, filtered.state)
+ else:
+ self.assertEqual(unf, filtered.state)
+
def test_throttle(self):
"""Test if lowpass filter works."""
filt = ThrottleFilter(window_size=3,
diff --git a/tests/components/sensor/test_geo_rss_events.py b/tests/components/sensor/test_geo_rss_events.py
index 21538d458bc..3362f799392 100644
--- a/tests/components/sensor/test_geo_rss_events.py
+++ b/tests/components/sensor/test_geo_rss_events.py
@@ -123,6 +123,10 @@ class TestGeoRssServiceUpdater(unittest.TestCase):
assert len(all_states) == 1
state = self.hass.states.get("sensor.event_service_any")
assert int(state.state) == 0
+ assert state.attributes == {
+ ATTR_FRIENDLY_NAME: "Event Service Any",
+ ATTR_UNIT_OF_MEASUREMENT: "Events",
+ ATTR_ICON: "mdi:alert"}
@mock.patch('georss_client.generic_feed.GenericFeed')
def test_setup_with_categories(self, mock_feed):
diff --git a/tests/components/sensor/test_jewish_calendar.py b/tests/components/sensor/test_jewish_calendar.py
index b67e340a9aa..ba3a11d862b 100644
--- a/tests/components/sensor/test_jewish_calendar.py
+++ b/tests/components/sensor/test_jewish_calendar.py
@@ -5,6 +5,7 @@ from datetime import datetime as dt
from unittest.mock import patch
from homeassistant.util.async_ import run_coroutine_threadsafe
+from homeassistant.util.dt import get_time_zone
from homeassistant.setup import setup_component
from homeassistant.components.sensor.jewish_calendar import JewishCalSensor
from tests.common import get_test_home_assistant
@@ -85,7 +86,7 @@ class TestJewishCalenderSensor(unittest.TestCase):
self.assertEqual(sensor.state, "כ\"ג באלול ה\' תשע\"ח")
def test_jewish_calendar_sensor_holiday_name(self):
- """Test Jewish calendar sensor date output in hebrew."""
+ """Test Jewish calendar sensor holiday name output in hebrew."""
test_time = dt(2018, 9, 10)
sensor = JewishCalSensor(
name='test', language='hebrew', sensor_type='holiday_name',
@@ -97,7 +98,7 @@ class TestJewishCalenderSensor(unittest.TestCase):
self.assertEqual(sensor.state, "א\' ראש השנה")
def test_jewish_calendar_sensor_holiday_name_english(self):
- """Test Jewish calendar sensor date output in hebrew."""
+ """Test Jewish calendar sensor holiday name output in english."""
test_time = dt(2018, 9, 10)
sensor = JewishCalSensor(
name='test', language='english', sensor_type='holiday_name',
@@ -109,7 +110,7 @@ class TestJewishCalenderSensor(unittest.TestCase):
self.assertEqual(sensor.state, "Rosh Hashana I")
def test_jewish_calendar_sensor_holyness(self):
- """Test Jewish calendar sensor date output in hebrew."""
+ """Test Jewish calendar sensor holyness value."""
test_time = dt(2018, 9, 10)
sensor = JewishCalSensor(
name='test', language='hebrew', sensor_type='holyness',
@@ -121,7 +122,7 @@ class TestJewishCalenderSensor(unittest.TestCase):
self.assertEqual(sensor.state, 1)
def test_jewish_calendar_sensor_torah_reading(self):
- """Test Jewish calendar sensor date output in hebrew."""
+ """Test Jewish calendar sensor torah reading in hebrew."""
test_time = dt(2018, 9, 8)
sensor = JewishCalSensor(
name='test', language='hebrew', sensor_type='weekly_portion',
@@ -133,19 +134,19 @@ class TestJewishCalenderSensor(unittest.TestCase):
self.assertEqual(sensor.state, "פרשת נצבים")
def test_jewish_calendar_sensor_first_stars_ny(self):
- """Test Jewish calendar sensor date output in hebrew."""
+ """Test Jewish calendar sensor first stars time in NY, US."""
test_time = dt(2018, 9, 8)
sensor = JewishCalSensor(
name='test', language='hebrew', sensor_type='first_stars',
latitude=40.7128, longitude=-74.0060,
- timezone="America/New_York", diaspora=False)
+ timezone=get_time_zone("America/New_York"), diaspora=False)
with patch('homeassistant.util.dt.now', return_value=test_time):
run_coroutine_threadsafe(
sensor.async_update(), self.hass.loop).result()
self.assertEqual(sensor.state, time(19, 48))
def test_jewish_calendar_sensor_first_stars_jerusalem(self):
- """Test Jewish calendar sensor date output in hebrew."""
+ """Test Jewish calendar sensor first stars time in Jerusalem, IL."""
test_time = dt(2018, 9, 8)
sensor = JewishCalSensor(
name='test', language='hebrew', sensor_type='first_stars',
@@ -155,3 +156,15 @@ class TestJewishCalenderSensor(unittest.TestCase):
run_coroutine_threadsafe(
sensor.async_update(), self.hass.loop).result()
self.assertEqual(sensor.state, time(19, 21))
+
+ def test_jewish_calendar_sensor_torah_reading_weekday(self):
+ """Test the sensor showing torah reading also on weekdays."""
+ test_time = dt(2018, 10, 14)
+ sensor = JewishCalSensor(
+ name='test', language='hebrew', sensor_type='weekly_portion',
+ latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE,
+ timezone="Asia/Jerusalem", diaspora=False)
+ with patch('homeassistant.util.dt.now', return_value=test_time):
+ run_coroutine_threadsafe(
+ sensor.async_update(), self.hass.loop).result()
+ self.assertEqual(sensor.state, "פרשת לך לך")
diff --git a/tests/components/sensor/test_moldindicator.py b/tests/components/sensor/test_moldindicator.py
index 4f1b40bf9ef..7b2480f1298 100644
--- a/tests/components/sensor/test_moldindicator.py
+++ b/tests/components/sensor/test_moldindicator.py
@@ -5,8 +5,8 @@ from homeassistant.setup import setup_component
import homeassistant.components.sensor as sensor
from homeassistant.components.sensor.mold_indicator import (ATTR_DEWPOINT,
ATTR_CRITICAL_TEMP)
-from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT,
- TEMP_CELSIUS)
+from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS)
from tests.common import get_test_home_assistant
@@ -44,7 +44,7 @@ class TestSensorMoldIndicator(unittest.TestCase):
assert moldind
assert '%' == moldind.attributes.get('unit_of_measurement')
- def test_invalidhum(self):
+ def test_invalidcalib(self):
"""Test invalid sensor values."""
self.hass.states.set('test.indoortemp', '10',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
@@ -53,6 +53,32 @@ class TestSensorMoldIndicator(unittest.TestCase):
self.hass.states.set('test.indoorhumidity', '0',
{ATTR_UNIT_OF_MEASUREMENT: '%'})
+ self.assertTrue(setup_component(self.hass, sensor.DOMAIN, {
+ 'sensor': {
+ 'platform': 'mold_indicator',
+ 'indoor_temp_sensor': 'test.indoortemp',
+ 'outdoor_temp_sensor': 'test.outdoortemp',
+ 'indoor_humidity_sensor': 'test.indoorhumidity',
+ 'calibration_factor': 0
+ }
+ }))
+ self.hass.start()
+ self.hass.block_till_done()
+ moldind = self.hass.states.get('sensor.mold_indicator')
+ assert moldind
+ assert moldind.state == 'unavailable'
+ assert moldind.attributes.get(ATTR_DEWPOINT) is None
+ assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None
+
+ def test_invalidhum(self):
+ """Test invalid sensor values."""
+ self.hass.states.set('test.indoortemp', '10',
+ {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
+ self.hass.states.set('test.outdoortemp', '10',
+ {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
+ self.hass.states.set('test.indoorhumidity', '-1',
+ {ATTR_UNIT_OF_MEASUREMENT: '%'})
+
self.assertTrue(setup_component(self.hass, sensor.DOMAIN, {
'sensor': {
'platform': 'mold_indicator',
@@ -62,9 +88,32 @@ class TestSensorMoldIndicator(unittest.TestCase):
'calibration_factor': 2.0
}
}))
+
+ self.hass.start()
+ self.hass.block_till_done()
moldind = self.hass.states.get('sensor.mold_indicator')
assert moldind
- assert moldind.state == '0'
+ assert moldind.state == 'unavailable'
+ assert moldind.attributes.get(ATTR_DEWPOINT) is None
+ assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None
+
+ self.hass.states.set('test.indoorhumidity', 'A',
+ {ATTR_UNIT_OF_MEASUREMENT: '%'})
+ self.hass.block_till_done()
+ moldind = self.hass.states.get('sensor.mold_indicator')
+ assert moldind
+ assert moldind.state == 'unavailable'
+ assert moldind.attributes.get(ATTR_DEWPOINT) is None
+ assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None
+
+ self.hass.states.set('test.indoorhumidity', '10',
+ {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
+ self.hass.block_till_done()
+ moldind = self.hass.states.get('sensor.mold_indicator')
+ assert moldind
+ assert moldind.state == 'unavailable'
+ assert moldind.attributes.get(ATTR_DEWPOINT) is None
+ assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None
def test_calculation(self):
"""Test the mold indicator internal calculations."""
@@ -77,7 +126,8 @@ class TestSensorMoldIndicator(unittest.TestCase):
'calibration_factor': 2.0
}
}))
-
+ self.hass.start()
+ self.hass.block_till_done()
moldind = self.hass.states.get('sensor.mold_indicator')
assert moldind
@@ -98,6 +148,66 @@ class TestSensorMoldIndicator(unittest.TestCase):
assert state
assert state == '68'
+ def test_unknown_sensor(self):
+ """Test the sensor_changed function."""
+ self.assertTrue(setup_component(self.hass, sensor.DOMAIN, {
+ 'sensor': {
+ 'platform': 'mold_indicator',
+ 'indoor_temp_sensor': 'test.indoortemp',
+ 'outdoor_temp_sensor': 'test.outdoortemp',
+ 'indoor_humidity_sensor': 'test.indoorhumidity',
+ 'calibration_factor': 2.0
+ }
+ }))
+ self.hass.start()
+
+ self.hass.states.set('test.indoortemp', STATE_UNKNOWN,
+ {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
+ self.hass.block_till_done()
+ moldind = self.hass.states.get('sensor.mold_indicator')
+ assert moldind
+ assert moldind.state == 'unavailable'
+ assert moldind.attributes.get(ATTR_DEWPOINT) is None
+ assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None
+
+ self.hass.states.set('test.indoortemp', '30',
+ {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
+ self.hass.states.set('test.outdoortemp', STATE_UNKNOWN,
+ {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
+ self.hass.block_till_done()
+ moldind = self.hass.states.get('sensor.mold_indicator')
+ assert moldind
+ assert moldind.state == 'unavailable'
+ assert moldind.attributes.get(ATTR_DEWPOINT) is None
+ assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None
+
+ self.hass.states.set('test.outdoortemp', '25',
+ {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
+ self.hass.states.set('test.indoorhumidity', STATE_UNKNOWN,
+ {ATTR_UNIT_OF_MEASUREMENT: '%'})
+ self.hass.block_till_done()
+ moldind = self.hass.states.get('sensor.mold_indicator')
+ assert moldind
+ assert moldind.state == 'unavailable'
+ assert moldind.attributes.get(ATTR_DEWPOINT) is None
+ assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None
+
+ self.hass.states.set('test.indoorhumidity', '20',
+ {ATTR_UNIT_OF_MEASUREMENT: '%'})
+ self.hass.block_till_done()
+ moldind = self.hass.states.get('sensor.mold_indicator')
+ assert moldind
+ assert moldind.state == '23'
+
+ dewpoint = moldind.attributes.get(ATTR_DEWPOINT)
+ assert dewpoint
+ assert dewpoint > 4.58
+ assert dewpoint < 4.59
+
+ esttemp = moldind.attributes.get(ATTR_CRITICAL_TEMP)
+ assert esttemp
+ assert esttemp == 27.5
+
def test_sensor_changed(self):
"""Test the sensor_changed function."""
self.assertTrue(setup_component(self.hass, sensor.DOMAIN, {
@@ -109,6 +219,7 @@ class TestSensorMoldIndicator(unittest.TestCase):
'calibration_factor': 2.0
}
}))
+ self.hass.start()
self.hass.states.set('test.indoortemp', '30',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
@@ -121,6 +232,6 @@ class TestSensorMoldIndicator(unittest.TestCase):
assert self.hass.states.get('sensor.mold_indicator').state == '57'
self.hass.states.set('test.indoorhumidity', '20',
- {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
+ {ATTR_UNIT_OF_MEASUREMENT: '%'})
self.hass.block_till_done()
assert self.hass.states.get('sensor.mold_indicator').state == '23'
diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py
index 4f70c37e04f..873de5a9bd6 100644
--- a/tests/components/sensor/test_mqtt.py
+++ b/tests/components/sensor/test_mqtt.py
@@ -1,4 +1,5 @@
"""The tests for the MQTT sensor platform."""
+import json
import unittest
from datetime import timedelta, datetime
@@ -411,3 +412,41 @@ async def test_discovery_removal_sensor(hass, mqtt_mock, caplog):
await hass.async_block_till_done()
state = hass.states.get('sensor.beer')
assert state is None
+
+
+async def test_entity_device_info_with_identifier(hass, mqtt_mock):
+ """Test MQTT sensor device registry integration."""
+ entry = MockConfigEntry(domain=mqtt.DOMAIN)
+ entry.add_to_hass(hass)
+ await async_start(hass, 'homeassistant', {}, entry)
+ registry = await hass.helpers.device_registry.async_get_registry()
+
+ data = json.dumps({
+ 'platform': 'mqtt',
+ 'name': 'Test 1',
+ 'state_topic': 'test-topic',
+ 'device': {
+ 'identifiers': ['helloworld'],
+ 'connections': [
+ ["mac", "02:5b:26:a8:dc:12"],
+ ],
+ 'manufacturer': 'Whatever',
+ 'name': 'Beer',
+ 'model': 'Glass',
+ 'sw_version': '0.1-beta',
+ },
+ 'unique_id': 'veryunique'
+ })
+ async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config',
+ data)
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({('mqtt', 'helloworld')}, set())
+ assert device is not None
+ assert device.identifiers == {('mqtt', 'helloworld')}
+ assert device.connections == {('mac', "02:5b:26:a8:dc:12")}
+ assert device.manufacturer == 'Whatever'
+ assert device.name == 'Beer'
+ assert device.model == 'Glass'
+ assert device.sw_version == '0.1-beta'
diff --git a/tests/components/sensor/test_rmvtransport.py b/tests/components/sensor/test_rmvtransport.py
index 9db19ecde49..d917edf0029 100644
--- a/tests/components/sensor/test_rmvtransport.py
+++ b/tests/components/sensor/test_rmvtransport.py
@@ -1,14 +1,17 @@
"""The tests for the rmvtransport platform."""
-import unittest
-from unittest.mock import patch
import datetime
+from unittest.mock import patch
-from homeassistant.setup import setup_component
+from homeassistant.setup import async_setup_component
-from tests.common import get_test_home_assistant
+from tests.common import mock_coro
-VALID_CONFIG_MINIMAL = {'sensor': {'platform': 'rmvtransport',
- 'next_departure': [{'station': '3000010'}]}}
+
+VALID_CONFIG_MINIMAL = {'sensor': {
+ 'platform': 'rmvtransport',
+ 'next_departure': [
+ {'station': '3000010'}
+ ]}}
VALID_CONFIG_NAME = {'sensor': {
'platform': 'rmvtransport',
@@ -41,8 +44,7 @@ VALID_CONFIG_DEST = {'sensor': {
]}}
-def get_departuresMock(stationId, maxJourneys,
- products): # pylint: disable=invalid-name
+def get_departures_mock():
"""Mock rmvtransport departures loading."""
data = {'station': 'Frankfurt (Main) Hauptbahnhof',
'stationId': '3000010', 'filter': '11111111111', 'journeys': [
@@ -97,77 +99,77 @@ def get_departuresMock(stationId, maxJourneys,
return data
-def get_errDeparturesMock(stationId, maxJourneys,
- products): # pylint: disable=invalid-name
- """Mock rmvtransport departures erroneous loading."""
- raise ValueError
+def get_no_departures_mock():
+ """Mock no departures in results."""
+ data = {'station': 'Frankfurt (Main) Hauptbahnhof',
+ 'stationId': '3000010',
+ 'filter': '11111111111',
+ 'journeys': []}
+ return data
-class TestRMVtransportSensor(unittest.TestCase):
- """Test the rmvtransport sensor."""
+async def test_rmvtransport_min_config(hass):
+ """Test minimal rmvtransport configuration."""
+ with patch('RMVtransport.RMVtransport.get_departures',
+ return_value=mock_coro(get_departures_mock())):
+ assert await async_setup_component(hass, 'sensor',
+ VALID_CONFIG_MINIMAL) is True
- def setUp(self):
- """Set up things to run when tests begin."""
- self.hass = get_test_home_assistant()
- self.config = VALID_CONFIG_MINIMAL
- self.reference = {}
- self.entities = []
+ state = hass.states.get('sensor.frankfurt_main_hauptbahnhof')
+ assert state.state == '7'
+ assert state.attributes['departure_time'] == \
+ datetime.datetime(2018, 8, 6, 14, 21)
+ assert state.attributes['direction'] == \
+ 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife'
+ assert state.attributes['product'] == 'Tram'
+ assert state.attributes['line'] == 12
+ assert state.attributes['icon'] == 'mdi:tram'
+ assert state.attributes['friendly_name'] == 'Frankfurt (Main) Hauptbahnhof'
- def tearDown(self):
- """Stop everything that was started."""
- self.hass.stop()
- @patch('RMVtransport.RMVtransport.get_departures',
- side_effect=get_departuresMock)
- def test_rmvtransport_min_config(self, mock_get_departures):
- """Test minimal rmvtransport configuration."""
- assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL)
- state = self.hass.states.get('sensor.frankfurt_main_hauptbahnhof')
- self.assertEqual(state.state, '7')
- self.assertEqual(state.attributes['departure_time'],
- datetime.datetime(2018, 8, 6, 14, 21))
- self.assertEqual(state.attributes['direction'],
- 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife')
- self.assertEqual(state.attributes['product'], 'Tram')
- self.assertEqual(state.attributes['line'], 12)
- self.assertEqual(state.attributes['icon'], 'mdi:tram')
- self.assertEqual(state.attributes['friendly_name'],
- 'Frankfurt (Main) Hauptbahnhof')
+async def test_rmvtransport_name_config(hass):
+ """Test custom name configuration."""
+ with patch('RMVtransport.RMVtransport.get_departures',
+ return_value=mock_coro(get_departures_mock())):
+ assert await async_setup_component(hass, 'sensor', VALID_CONFIG_NAME)
- @patch('RMVtransport.RMVtransport.get_departures',
- side_effect=get_departuresMock)
- def test_rmvtransport_name_config(self, mock_get_departures):
- """Test custom name configuration."""
- assert setup_component(self.hass, 'sensor', VALID_CONFIG_NAME)
- state = self.hass.states.get('sensor.my_station')
- self.assertEqual(state.attributes['friendly_name'], 'My Station')
+ state = hass.states.get('sensor.my_station')
+ assert state.attributes['friendly_name'] == 'My Station'
- @patch('RMVtransport.RMVtransport.get_departures',
- side_effect=get_errDeparturesMock)
- def test_rmvtransport_err_config(self, mock_get_departures):
- """Test erroneous rmvtransport configuration."""
- assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL)
- @patch('RMVtransport.RMVtransport.get_departures',
- side_effect=get_departuresMock)
- def test_rmvtransport_misc_config(self, mock_get_departures):
- """Test misc configuration."""
- assert setup_component(self.hass, 'sensor', VALID_CONFIG_MISC)
- state = self.hass.states.get('sensor.frankfurt_main_hauptbahnhof')
- self.assertEqual(state.attributes['friendly_name'],
- 'Frankfurt (Main) Hauptbahnhof')
- self.assertEqual(state.attributes['line'], 21)
+async def test_rmvtransport_misc_config(hass):
+ """Test misc configuration."""
+ with patch('RMVtransport.RMVtransport.get_departures',
+ return_value=mock_coro(get_departures_mock())):
+ assert await async_setup_component(hass, 'sensor', VALID_CONFIG_MISC)
- @patch('RMVtransport.RMVtransport.get_departures',
- side_effect=get_departuresMock)
- def test_rmvtransport_dest_config(self, mock_get_departures):
- """Test misc configuration."""
- assert setup_component(self.hass, 'sensor', VALID_CONFIG_DEST)
- state = self.hass.states.get('sensor.frankfurt_main_hauptbahnhof')
- self.assertEqual(state.state, '11')
- self.assertEqual(state.attributes['direction'],
- 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife')
- self.assertEqual(state.attributes['line'], 12)
- self.assertEqual(state.attributes['minutes'], 11)
- self.assertEqual(state.attributes['departure_time'],
- datetime.datetime(2018, 8, 6, 14, 25))
+ state = hass.states.get('sensor.frankfurt_main_hauptbahnhof')
+ assert state.attributes['friendly_name'] == 'Frankfurt (Main) Hauptbahnhof'
+ assert state.attributes['line'] == 21
+
+
+async def test_rmvtransport_dest_config(hass):
+ """Test destination configuration."""
+ with patch('RMVtransport.RMVtransport.get_departures',
+ return_value=mock_coro(get_departures_mock())):
+ assert await async_setup_component(hass, 'sensor', VALID_CONFIG_DEST)
+
+ state = hass.states.get('sensor.frankfurt_main_hauptbahnhof')
+ assert state.state == '11'
+ assert state.attributes['direction'] == \
+ 'Frankfurt (Main) Hugo-Junkers-Straße/Schleife'
+ assert state.attributes['line'] == 12
+ assert state.attributes['minutes'] == 11
+ assert state.attributes['departure_time'] == \
+ datetime.datetime(2018, 8, 6, 14, 25)
+
+
+async def test_rmvtransport_no_departures(hass):
+ """Test for no departures."""
+ with patch('RMVtransport.RMVtransport.get_departures',
+ return_value=mock_coro(get_no_departures_mock())):
+ assert await async_setup_component(hass, 'sensor',
+ VALID_CONFIG_MINIMAL)
+
+ state = hass.states.get('sensor.frankfurt_main_hauptbahnhof')
+ assert not state
diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py
index 2211f092d7b..b0b1ea22285 100644
--- a/tests/components/sensor/test_template.py
+++ b/tests/components/sensor/test_template.py
@@ -1,5 +1,6 @@
"""The test for the Template sensor platform."""
-from homeassistant.setup import setup_component
+from homeassistant.const import EVENT_HOMEASSISTANT_START
+from homeassistant.setup import setup_component, async_setup_component
from tests.common import get_test_home_assistant, assert_setup_component
@@ -52,7 +53,8 @@ class TestTemplateSensor:
'platform': 'template',
'sensors': {
'test_template_sensor': {
- 'value_template': "State",
+ 'value_template':
+ "{{ states.sensor.test_state.state }}",
'icon_template':
"{% if states.sensor.test_state.state == "
"'Works' %}"
@@ -82,7 +84,8 @@ class TestTemplateSensor:
'platform': 'template',
'sensors': {
'test_template_sensor': {
- 'value_template': "State",
+ 'value_template':
+ "{{ states.sensor.test_state.state }}",
'entity_picture_template':
"{% if states.sensor.test_state.state == "
"'Works' %}"
@@ -112,7 +115,8 @@ class TestTemplateSensor:
'platform': 'template',
'sensors': {
'test_template_sensor': {
- 'value_template': "State",
+ 'value_template':
+ "{{ states.sensor.test_state.state }}",
'friendly_name_template':
"It {{ states.sensor.test_state.state }}."
}
@@ -276,7 +280,8 @@ class TestTemplateSensor:
'platform': 'template',
'sensors': {
'test': {
- 'value_template': '{{ foo }}',
+ 'value_template':
+ '{{ states.sensor.test_sensor.state }}',
'device_class': 'foobarnotreal',
},
},
@@ -291,10 +296,14 @@ class TestTemplateSensor:
'platform': 'template',
'sensors': {
'test1': {
- 'value_template': '{{ foo }}',
+ 'value_template':
+ '{{ states.sensor.test_sensor.state }}',
'device_class': 'temperature',
},
- 'test2': {'value_template': '{{ foo }}'},
+ 'test2': {
+ 'value_template':
+ '{{ states.sensor.test_sensor.state }}'
+ },
}
}
})
@@ -304,3 +313,83 @@ class TestTemplateSensor:
assert state.attributes['device_class'] == 'temperature'
state = self.hass.states.get('sensor.test2')
assert 'device_class' not in state.attributes
+
+
+async def test_no_template_match_all(hass, caplog):
+ """Test that we do not allow sensors that match on all."""
+ hass.states.async_set('sensor.test_sensor', 'startup')
+
+ await async_setup_component(hass, 'sensor', {
+ 'sensor': {
+ 'platform': 'template',
+ 'sensors': {
+ 'invalid_state': {
+ 'value_template': '{{ 1 + 1 }}',
+ },
+ 'invalid_icon': {
+ 'value_template':
+ '{{ states.sensor.test_sensor.state }}',
+ 'icon_template': '{{ 1 + 1 }}',
+ },
+ 'invalid_entity_picture': {
+ 'value_template':
+ '{{ states.sensor.test_sensor.state }}',
+ 'entity_picture_template': '{{ 1 + 1 }}',
+ },
+ 'invalid_friendly_name': {
+ 'value_template':
+ '{{ states.sensor.test_sensor.state }}',
+ 'friendly_name_template': '{{ 1 + 1 }}',
+ },
+ }
+ }
+ })
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 5
+ assert ('Template sensor invalid_state has no entity ids '
+ 'configured to track nor were we able to extract the entities to '
+ 'track from the value template') in caplog.text
+ assert ('Template sensor invalid_icon has no entity ids '
+ 'configured to track nor were we able to extract the entities to '
+ 'track from the icon template') in caplog.text
+ assert ('Template sensor invalid_entity_picture has no entity ids '
+ 'configured to track nor were we able to extract the entities to '
+ 'track from the entity_picture template') in caplog.text
+ assert ('Template sensor invalid_friendly_name has no entity ids '
+ 'configured to track nor were we able to extract the entities to '
+ 'track from the friendly_name template') in caplog.text
+
+ assert hass.states.get('sensor.invalid_state').state == 'unknown'
+ assert hass.states.get('sensor.invalid_icon').state == 'unknown'
+ assert hass.states.get('sensor.invalid_entity_picture').state == 'unknown'
+ assert hass.states.get('sensor.invalid_friendly_name').state == 'unknown'
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ await hass.async_block_till_done()
+
+ assert hass.states.get('sensor.invalid_state').state == '2'
+ assert hass.states.get('sensor.invalid_icon').state == 'startup'
+ assert hass.states.get('sensor.invalid_entity_picture').state == 'startup'
+ assert hass.states.get('sensor.invalid_friendly_name').state == 'startup'
+
+ hass.states.async_set('sensor.test_sensor', 'hello')
+ await hass.async_block_till_done()
+
+ assert hass.states.get('sensor.invalid_state').state == '2'
+ assert hass.states.get('sensor.invalid_icon').state == 'startup'
+ assert hass.states.get('sensor.invalid_entity_picture').state == 'startup'
+ assert hass.states.get('sensor.invalid_friendly_name').state == 'startup'
+
+ await hass.helpers.entity_component.async_update_entity(
+ 'sensor.invalid_state')
+ await hass.helpers.entity_component.async_update_entity(
+ 'sensor.invalid_icon')
+ await hass.helpers.entity_component.async_update_entity(
+ 'sensor.invalid_entity_picture')
+ await hass.helpers.entity_component.async_update_entity(
+ 'sensor.invalid_friendly_name')
+
+ assert hass.states.get('sensor.invalid_state').state == '2'
+ assert hass.states.get('sensor.invalid_icon').state == 'hello'
+ assert hass.states.get('sensor.invalid_entity_picture').state == 'hello'
+ assert hass.states.get('sensor.invalid_friendly_name').state == 'hello'
diff --git a/tests/components/sensor/test_transport_nsw.py b/tests/components/sensor/test_transport_nsw.py
new file mode 100644
index 00000000000..fe933272962
--- /dev/null
+++ b/tests/components/sensor/test_transport_nsw.py
@@ -0,0 +1,50 @@
+"""The tests for the Transport NSW (AU) sensor platform."""
+import unittest
+from unittest.mock import patch
+
+from homeassistant.setup import setup_component
+
+from tests.common import get_test_home_assistant
+
+VALID_CONFIG = {'sensor': {
+ 'platform': 'transport_nsw',
+ 'stop_id': '209516',
+ 'route': '199',
+ 'api_key': 'YOUR_API_KEY'}
+ }
+
+
+def get_departuresMock(_stop_id, route, api_key):
+ """Mock TransportNSW departures loading."""
+ data = {
+ 'stop_id': '209516',
+ 'route': '199',
+ 'due': 16,
+ 'delay': 6,
+ 'real_time': 'y'
+ }
+ return data
+
+
+class TestRMVtransportSensor(unittest.TestCase):
+ """Test the TransportNSW sensor."""
+
+ def setUp(self):
+ """Set up things to run when tests begin."""
+ self.hass = get_test_home_assistant()
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ @patch('TransportNSW.TransportNSW.get_departures',
+ side_effect=get_departuresMock)
+ def test_transportnsw_config(self, mock_get_departures):
+ """Test minimal TransportNSW configuration."""
+ assert setup_component(self.hass, 'sensor', VALID_CONFIG)
+ state = self.hass.states.get('sensor.next_bus')
+ self.assertEqual(state.state, '16')
+ self.assertEqual(state.attributes['stop_id'], '209516')
+ self.assertEqual(state.attributes['route'], '199')
+ self.assertEqual(state.attributes['delay'], 6)
+ self.assertEqual(state.attributes['real_time'], 'y')
diff --git a/tests/components/simplisafe/__init__.py b/tests/components/simplisafe/__init__.py
new file mode 100644
index 00000000000..b1cc391eec9
--- /dev/null
+++ b/tests/components/simplisafe/__init__.py
@@ -0,0 +1 @@
+"""Define tests for the SimpliSafe component."""
diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py
new file mode 100644
index 00000000000..63b932ee681
--- /dev/null
+++ b/tests/components/simplisafe/test_config_flow.py
@@ -0,0 +1,120 @@
+"""Define tests for the SimpliSafe config flow."""
+import json
+from datetime import timedelta
+from unittest.mock import mock_open, patch, MagicMock, PropertyMock
+
+from homeassistant import data_entry_flow
+from homeassistant.components.simplisafe import DOMAIN, config_flow
+from homeassistant.const import (
+ CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME)
+
+from tests.common import MockConfigEntry, mock_coro
+
+
+def mock_api():
+ """Mock SimpliSafe API class."""
+ api = MagicMock()
+ type(api).refresh_token = PropertyMock(return_value='12345abc')
+ return api
+
+
+async def test_duplicate_error(hass):
+ """Test that errors are shown when duplicates are added."""
+ conf = {
+ CONF_USERNAME: 'user@email.com',
+ CONF_PASSWORD: 'password',
+ }
+
+ MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass)
+ flow = config_flow.SimpliSafeFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_user(user_input=conf)
+ assert result['errors'] == {CONF_USERNAME: 'identifier_exists'}
+
+
+async def test_invalid_credentials(hass):
+ """Test that invalid credentials throws an error."""
+ from simplipy.errors import SimplipyError
+ conf = {
+ CONF_USERNAME: 'user@email.com',
+ CONF_PASSWORD: 'password',
+ }
+
+ flow = config_flow.SimpliSafeFlowHandler()
+ flow.hass = hass
+
+ with patch('simplipy.API.login_via_credentials',
+ return_value=mock_coro(exception=SimplipyError)):
+ result = await flow.async_step_user(user_input=conf)
+ assert result['errors'] == {'base': 'invalid_credentials'}
+
+
+async def test_show_form(hass):
+ """Test that the form is served with no input."""
+ flow = config_flow.SimpliSafeFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_user(user_input=None)
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'user'
+
+
+async def test_step_import(hass):
+ """Test that the import step works."""
+ conf = {
+ CONF_USERNAME: 'user@email.com',
+ CONF_PASSWORD: 'password',
+ }
+
+ flow = config_flow.SimpliSafeFlowHandler()
+ flow.hass = hass
+
+ mop = mock_open(read_data=json.dumps({'refresh_token': '12345'}))
+
+ with patch('simplipy.API.login_via_credentials',
+ return_value=mock_coro(return_value=mock_api())):
+ with patch('homeassistant.util.json.open', mop, create=True):
+ with patch('homeassistant.util.json.os.open', return_value=0):
+ with patch('homeassistant.util.json.os.replace'):
+ result = await flow.async_step_import(import_config=conf)
+
+ assert result[
+ 'type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result['title'] == 'user@email.com'
+ assert result['data'] == {
+ CONF_USERNAME: 'user@email.com',
+ CONF_TOKEN: '12345abc',
+ CONF_SCAN_INTERVAL: 30,
+ }
+
+
+async def test_step_user(hass):
+ """Test that the user step works."""
+ conf = {
+ CONF_USERNAME: 'user@email.com',
+ CONF_PASSWORD: 'password',
+ CONF_SCAN_INTERVAL: timedelta(seconds=90),
+ }
+
+ flow = config_flow.SimpliSafeFlowHandler()
+ flow.hass = hass
+
+ mop = mock_open(read_data=json.dumps({'refresh_token': '12345'}))
+
+ with patch('simplipy.API.login_via_credentials',
+ return_value=mock_coro(return_value=mock_api())):
+ with patch('homeassistant.util.json.open', mop, create=True):
+ with patch('homeassistant.util.json.os.open', return_value=0):
+ with patch('homeassistant.util.json.os.replace'):
+ result = await flow.async_step_user(user_input=conf)
+
+ assert result[
+ 'type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result['title'] == 'user@email.com'
+ assert result['data'] == {
+ CONF_USERNAME: 'user@email.com',
+ CONF_TOKEN: '12345abc',
+ CONF_SCAN_INTERVAL: 90,
+ }
diff --git a/tests/components/smhi/__init__.py b/tests/components/smhi/__init__.py
new file mode 100644
index 00000000000..100b1f1bbb1
--- /dev/null
+++ b/tests/components/smhi/__init__.py
@@ -0,0 +1 @@
+"""Tests for the SMHI component."""
diff --git a/tests/components/smhi/common.py b/tests/components/smhi/common.py
new file mode 100644
index 00000000000..ecf904ac9c9
--- /dev/null
+++ b/tests/components/smhi/common.py
@@ -0,0 +1,11 @@
+"""Common test utilities."""
+from unittest.mock import Mock
+
+
+class AsyncMock(Mock):
+ """Implements Mock async."""
+
+ # pylint: disable=W0235
+ async def __call__(self, *args, **kwargs):
+ """Hack for async support for Mock."""
+ return super(AsyncMock, self).__call__(*args, **kwargs)
diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py
new file mode 100644
index 00000000000..b4e543231d9
--- /dev/null
+++ b/tests/components/smhi/test_config_flow.py
@@ -0,0 +1,276 @@
+"""Tests for SMHI config flow."""
+from unittest.mock import Mock, patch
+
+from smhi.smhi_lib import Smhi as SmhiApi, SmhiForecastException
+
+from tests.common import mock_coro
+
+from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
+from homeassistant.components.smhi import config_flow
+
+
+# pylint: disable=W0212
+async def test_homeassistant_location_exists() -> None:
+ """Test if homeassistant location exists it should return True."""
+ hass = Mock()
+ flow = config_flow.SmhiFlowHandler()
+ flow.hass = hass
+ with patch.object(flow, '_check_location',
+ return_value=mock_coro(True)):
+ # Test exists
+ hass.config.location_name = 'Home'
+ hass.config.latitude = 17.8419
+ hass.config.longitude = 59.3262
+
+ assert await flow._homeassistant_location_exists() is True
+
+ # Test not exists
+ hass.config.location_name = None
+ hass.config.latitude = 0
+ hass.config.longitude = 0
+
+ assert await flow._homeassistant_location_exists() is False
+
+
+async def test_name_in_configuration_exists() -> None:
+ """Test if home location exists in configuration."""
+ hass = Mock()
+ flow = config_flow.SmhiFlowHandler()
+ flow.hass = hass
+
+ # Test exists
+ hass.config.location_name = 'Home'
+ hass.config.latitude = 17.8419
+ hass.config.longitude = 59.3262
+
+ # Check not exists
+ with patch.object(config_flow, 'smhi_locations',
+ return_value={
+ 'test': 'something', 'test2': 'something else'
+ }):
+
+ assert flow._name_in_configuration_exists('no_exist_name') is False
+
+ # Check exists
+ with patch.object(config_flow, 'smhi_locations',
+ return_value={
+ 'test': 'something', 'name_exist': 'config'
+ }):
+
+ assert flow._name_in_configuration_exists('name_exist') is True
+
+
+def test_smhi_locations(hass) -> None:
+ """Test return empty set."""
+ locations = config_flow.smhi_locations(hass)
+ assert not locations
+
+
+async def test_show_config_form() -> None:
+ """Test show configuration form."""
+ hass = Mock()
+ flow = config_flow.SmhiFlowHandler()
+ flow.hass = hass
+
+ result = await flow._show_config_form()
+
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'user'
+
+
+async def test_show_config_form_default_values() -> None:
+ """Test show configuration form."""
+ hass = Mock()
+ flow = config_flow.SmhiFlowHandler()
+ flow.hass = hass
+
+ result = await flow._show_config_form(
+ name="test", latitude='65', longitude='17')
+
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'user'
+
+
+async def test_flow_with_home_location(hass) -> None:
+ """Test config flow .
+
+ Tests the flow when a default location is configured
+ then it should return a form with default values
+ """
+ flow = config_flow.SmhiFlowHandler()
+ flow.hass = hass
+
+ with patch.object(flow, '_check_location',
+ return_value=mock_coro(True)):
+ hass.config.location_name = 'Home'
+ hass.config.latitude = 17.8419
+ hass.config.longitude = 59.3262
+
+ result = await flow.async_step_user()
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'user'
+
+
+async def test_flow_show_form() -> None:
+ """Test show form scenarios first time.
+
+ Test when the form should show when no configurations exists
+ """
+ hass = Mock()
+ flow = config_flow.SmhiFlowHandler()
+ flow.hass = hass
+
+ # Test show form when home assistant config exists and
+ # home is already configured, then new config is allowed
+ with \
+ patch.object(flow, '_show_config_form',
+ return_value=mock_coro()) as config_form, \
+ patch.object(flow, '_homeassistant_location_exists',
+ return_value=mock_coro(True)), \
+ patch.object(config_flow, 'smhi_locations',
+ return_value={
+ 'test': 'something', 'name_exist': 'config'
+ }):
+ await flow.async_step_user()
+ assert len(config_form.mock_calls) == 1
+
+ # Test show form when home assistant config not and
+ # home is not configured
+ with \
+ patch.object(flow, '_show_config_form',
+ return_value=mock_coro()) as config_form, \
+ patch.object(flow, '_homeassistant_location_exists',
+ return_value=mock_coro(False)), \
+ patch.object(config_flow, 'smhi_locations',
+ return_value={
+ 'test': 'something', 'name_exist': 'config'
+ }):
+
+ await flow.async_step_user()
+ assert len(config_form.mock_calls) == 1
+
+
+async def test_flow_show_form_name_exists() -> None:
+ """Test show form if name already exists.
+
+ Test when the form should show when no configurations exists
+ """
+ hass = Mock()
+ flow = config_flow.SmhiFlowHandler()
+ flow.hass = hass
+ test_data = {'name': 'home', CONF_LONGITUDE: '0', CONF_LATITUDE: '0'}
+ # Test show form when home assistant config exists and
+ # home is already configured, then new config is allowed
+ with \
+ patch.object(flow, '_show_config_form',
+ return_value=mock_coro()) as config_form, \
+ patch.object(flow, '_name_in_configuration_exists',
+ return_value=True), \
+ patch.object(config_flow, 'smhi_locations',
+ return_value={
+ 'test': 'something', 'name_exist': 'config'
+ }), \
+ patch.object(flow, '_check_location',
+ return_value=mock_coro(True)):
+
+ await flow.async_step_user(user_input=test_data)
+
+ assert len(config_form.mock_calls) == 1
+ assert len(flow._errors) == 1
+
+
+async def test_flow_entry_created_from_user_input() -> None:
+ """Test that create data from user input.
+
+ Test when the form should show when no configurations exists
+ """
+ hass = Mock()
+ flow = config_flow.SmhiFlowHandler()
+ flow.hass = hass
+
+ test_data = {'name': 'home', CONF_LONGITUDE: '0', CONF_LATITUDE: '0'}
+
+ # Test that entry created when user_input name not exists
+ with \
+ patch.object(flow, '_show_config_form',
+ return_value=mock_coro()) as config_form, \
+ patch.object(flow, '_name_in_configuration_exists',
+ return_value=False), \
+ patch.object(flow, '_homeassistant_location_exists',
+ return_value=mock_coro(False)), \
+ patch.object(config_flow, 'smhi_locations',
+ return_value={
+ 'test': 'something', 'name_exist': 'config'
+ }), \
+ patch.object(flow, '_check_location',
+ return_value=mock_coro(True)):
+
+ result = await flow.async_step_user(user_input=test_data)
+
+ assert result['type'] == 'create_entry'
+ assert result['data'] == test_data
+ assert not config_form.mock_calls
+
+
+async def test_flow_entry_created_user_input_faulty() -> None:
+ """Test that create data from user input and are faulty.
+
+ Test when the form should show when user puts faulty location
+ in the config gui. Then the form should show with error
+ """
+ hass = Mock()
+ flow = config_flow.SmhiFlowHandler()
+ flow.hass = hass
+
+ test_data = {'name': 'home', CONF_LONGITUDE: '0', CONF_LATITUDE: '0'}
+
+ # Test that entry created when user_input name not exists
+ with \
+ patch.object(flow, '_check_location',
+ return_value=mock_coro(True)), \
+ patch.object(flow, '_show_config_form',
+ return_value=mock_coro()) as config_form, \
+ patch.object(flow, '_name_in_configuration_exists',
+ return_value=False), \
+ patch.object(flow, '_homeassistant_location_exists',
+ return_value=mock_coro(False)), \
+ patch.object(config_flow, 'smhi_locations',
+ return_value={
+ 'test': 'something', 'name_exist': 'config'
+ }), \
+ patch.object(flow, '_check_location',
+ return_value=mock_coro(False)):
+
+ await flow.async_step_user(user_input=test_data)
+
+ assert len(config_form.mock_calls) == 1
+ assert len(flow._errors) == 1
+
+
+async def test_check_location_correct() -> None:
+ """Test check location when correct input."""
+ hass = Mock()
+ flow = config_flow.SmhiFlowHandler()
+ flow.hass = hass
+
+ with \
+ patch.object(config_flow.aiohttp_client, 'async_get_clientsession'),\
+ patch.object(SmhiApi, 'async_get_forecast',
+ return_value=mock_coro()):
+
+ assert await flow._check_location('58', '17') is True
+
+
+async def test_check_location_faulty() -> None:
+ """Test check location when faulty input."""
+ hass = Mock()
+ flow = config_flow.SmhiFlowHandler()
+ flow.hass = hass
+
+ with \
+ patch.object(config_flow.aiohttp_client,
+ 'async_get_clientsession'), \
+ patch.object(SmhiApi, 'async_get_forecast',
+ side_effect=SmhiForecastException()):
+
+ assert await flow._check_location('58', '17') is False
diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py
new file mode 100644
index 00000000000..2b0dbaabafd
--- /dev/null
+++ b/tests/components/smhi/test_init.py
@@ -0,0 +1,39 @@
+"""Test SMHI component setup process."""
+from unittest.mock import Mock
+
+from homeassistant.components import smhi
+
+from .common import AsyncMock
+
+TEST_CONFIG = {
+ "config": {
+ "name": "0123456789ABCDEF",
+ "longitude": "62.0022",
+ "latitude": "17.0022"
+ }
+}
+
+
+async def test_setup_always_return_true() -> None:
+ """Test async_setup always returns True."""
+ hass = Mock()
+ # Returns true with empty config
+ assert await smhi.async_setup(hass, {}) is True
+
+ # Returns true with a config provided
+ assert await smhi.async_setup(hass, TEST_CONFIG) is True
+
+
+async def test_forward_async_setup_entry() -> None:
+ """Test that it will forward setup entry."""
+ hass = Mock()
+
+ assert await smhi.async_setup_entry(hass, {}) is True
+ assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1
+
+
+async def test_forward_async_unload_entry() -> None:
+ """Test that it will forward unload entry."""
+ hass = AsyncMock()
+ assert await smhi.async_unload_entry(hass, {}) is True
+ assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 1
diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py
index 69e65e24659..cb5207adb3e 100644
--- a/tests/components/switch/test_flux.py
+++ b/tests/components/switch/test_flux.py
@@ -183,22 +183,23 @@ class TestSwitchFlux(unittest.TestCase):
return sunrise_time
return sunset_time
- with patch('homeassistant.util.dt.now', return_value=test_time):
- with patch('homeassistant.helpers.sun.get_astral_event_date',
- side_effect=event_date):
- assert setup_component(self.hass, switch.DOMAIN, {
- switch.DOMAIN: {
- 'platform': 'flux',
- 'name': 'flux',
- 'lights': [dev1.entity_id]
- }
- })
- turn_on_calls = mock_service(
- self.hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(self.hass, 'switch.flux')
- self.hass.block_till_done()
- fire_time_changed(self.hass, test_time)
- self.hass.block_till_done()
+ with patch('homeassistant.components.switch.flux.dt_now',
+ return_value=test_time), \
+ patch('homeassistant.helpers.sun.get_astral_event_date',
+ side_effect=event_date):
+ assert setup_component(self.hass, switch.DOMAIN, {
+ switch.DOMAIN: {
+ 'platform': 'flux',
+ 'name': 'flux',
+ 'lights': [dev1.entity_id]
+ }
+ })
+ turn_on_calls = mock_service(
+ self.hass, light.DOMAIN, SERVICE_TURN_ON)
+ common.turn_on(self.hass, 'switch.flux')
+ self.hass.block_till_done()
+ fire_time_changed(self.hass, test_time)
+ self.hass.block_till_done()
call = turn_on_calls[-1]
self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 173)
self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.439, 0.37])
@@ -229,23 +230,24 @@ class TestSwitchFlux(unittest.TestCase):
return sunrise_time
return sunset_time
- with patch('homeassistant.util.dt.now', return_value=test_time):
- with patch('homeassistant.helpers.sun.get_astral_event_date',
- side_effect=event_date):
- assert setup_component(self.hass, switch.DOMAIN, {
- switch.DOMAIN: {
- 'platform': 'flux',
- 'name': 'flux',
- 'lights': [dev1.entity_id],
- 'stop_time': '22:00'
- }
- })
- turn_on_calls = mock_service(
- self.hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(self.hass, 'switch.flux')
- self.hass.block_till_done()
- fire_time_changed(self.hass, test_time)
- self.hass.block_till_done()
+ with patch('homeassistant.components.switch.flux.dt_now',
+ return_value=test_time), \
+ patch('homeassistant.helpers.sun.get_astral_event_date',
+ side_effect=event_date):
+ assert setup_component(self.hass, switch.DOMAIN, {
+ switch.DOMAIN: {
+ 'platform': 'flux',
+ 'name': 'flux',
+ 'lights': [dev1.entity_id],
+ 'stop_time': '22:00'
+ }
+ })
+ turn_on_calls = mock_service(
+ self.hass, light.DOMAIN, SERVICE_TURN_ON)
+ common.turn_on(self.hass, 'switch.flux')
+ self.hass.block_till_done()
+ fire_time_changed(self.hass, test_time)
+ self.hass.block_till_done()
call = turn_on_calls[-1]
self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 146)
self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.506, 0.385])
@@ -322,24 +324,25 @@ class TestSwitchFlux(unittest.TestCase):
return sunrise_time
return sunset_time
- with patch('homeassistant.util.dt.now', return_value=test_time):
- with patch('homeassistant.helpers.sun.get_astral_event_date',
- side_effect=event_date):
- assert setup_component(self.hass, switch.DOMAIN, {
- switch.DOMAIN: {
- 'platform': 'flux',
- 'name': 'flux',
- 'lights': [dev1.entity_id],
- 'start_time': '6:00',
- 'stop_time': '23:30'
- }
- })
- turn_on_calls = mock_service(
- self.hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(self.hass, 'switch.flux')
- self.hass.block_till_done()
- fire_time_changed(self.hass, test_time)
- self.hass.block_till_done()
+ with patch('homeassistant.components.switch.flux.dt_now',
+ return_value=test_time), \
+ patch('homeassistant.helpers.sun.get_astral_event_date',
+ side_effect=event_date):
+ assert setup_component(self.hass, switch.DOMAIN, {
+ switch.DOMAIN: {
+ 'platform': 'flux',
+ 'name': 'flux',
+ 'lights': [dev1.entity_id],
+ 'start_time': '6:00',
+ 'stop_time': '23:30'
+ }
+ })
+ turn_on_calls = mock_service(
+ self.hass, light.DOMAIN, SERVICE_TURN_ON)
+ common.turn_on(self.hass, 'switch.flux')
+ self.hass.block_till_done()
+ fire_time_changed(self.hass, test_time)
+ self.hass.block_till_done()
call = turn_on_calls[-1]
self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 147)
self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.504, 0.385])
@@ -372,23 +375,24 @@ class TestSwitchFlux(unittest.TestCase):
return sunrise_time
return sunset_time
- with patch('homeassistant.util.dt.now', return_value=test_time):
- with patch('homeassistant.helpers.sun.get_astral_event_date',
- side_effect=event_date):
- assert setup_component(self.hass, switch.DOMAIN, {
- switch.DOMAIN: {
- 'platform': 'flux',
- 'name': 'flux',
- 'lights': [dev1.entity_id],
- 'stop_time': '01:00'
- }
- })
- turn_on_calls = mock_service(
- self.hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(self.hass, 'switch.flux')
- self.hass.block_till_done()
- fire_time_changed(self.hass, test_time)
- self.hass.block_till_done()
+ with patch('homeassistant.components.switch.flux.dt_now',
+ return_value=test_time), \
+ patch('homeassistant.helpers.sun.get_astral_event_date',
+ side_effect=event_date):
+ assert setup_component(self.hass, switch.DOMAIN, {
+ switch.DOMAIN: {
+ 'platform': 'flux',
+ 'name': 'flux',
+ 'lights': [dev1.entity_id],
+ 'stop_time': '01:00'
+ }
+ })
+ turn_on_calls = mock_service(
+ self.hass, light.DOMAIN, SERVICE_TURN_ON)
+ common.turn_on(self.hass, 'switch.flux')
+ self.hass.block_till_done()
+ fire_time_changed(self.hass, test_time)
+ self.hass.block_till_done()
call = turn_on_calls[-1]
self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112)
self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379])
@@ -423,23 +427,24 @@ class TestSwitchFlux(unittest.TestCase):
return sunrise_time
return sunset_time
- with patch('homeassistant.util.dt.now', return_value=test_time):
- with patch('homeassistant.helpers.sun.get_astral_event_date',
- side_effect=event_date):
- assert setup_component(self.hass, switch.DOMAIN, {
- switch.DOMAIN: {
- 'platform': 'flux',
- 'name': 'flux',
- 'lights': [dev1.entity_id],
- 'stop_time': '01:00'
- }
- })
- turn_on_calls = mock_service(
- self.hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(self.hass, 'switch.flux')
- self.hass.block_till_done()
- fire_time_changed(self.hass, test_time)
- self.hass.block_till_done()
+ with patch('homeassistant.components.switch.flux.dt_now',
+ return_value=test_time), \
+ patch('homeassistant.helpers.sun.get_astral_event_date',
+ side_effect=event_date):
+ assert setup_component(self.hass, switch.DOMAIN, {
+ switch.DOMAIN: {
+ 'platform': 'flux',
+ 'name': 'flux',
+ 'lights': [dev1.entity_id],
+ 'stop_time': '01:00'
+ }
+ })
+ turn_on_calls = mock_service(
+ self.hass, light.DOMAIN, SERVICE_TURN_ON)
+ common.turn_on(self.hass, 'switch.flux')
+ self.hass.block_till_done()
+ fire_time_changed(self.hass, test_time)
+ self.hass.block_till_done()
call = turn_on_calls[-1]
self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 173)
self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.439, 0.37])
@@ -523,23 +528,24 @@ class TestSwitchFlux(unittest.TestCase):
return sunrise_time
return sunset_time
- with patch('homeassistant.util.dt.now', return_value=test_time):
- with patch('homeassistant.helpers.sun.get_astral_event_date',
- side_effect=event_date):
- assert setup_component(self.hass, switch.DOMAIN, {
- switch.DOMAIN: {
- 'platform': 'flux',
- 'name': 'flux',
- 'lights': [dev1.entity_id],
- 'stop_time': '01:00'
- }
- })
- turn_on_calls = mock_service(
- self.hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(self.hass, 'switch.flux')
- self.hass.block_till_done()
- fire_time_changed(self.hass, test_time)
- self.hass.block_till_done()
+ with patch('homeassistant.components.switch.flux.dt_now',
+ return_value=test_time), \
+ patch('homeassistant.helpers.sun.get_astral_event_date',
+ side_effect=event_date):
+ assert setup_component(self.hass, switch.DOMAIN, {
+ switch.DOMAIN: {
+ 'platform': 'flux',
+ 'name': 'flux',
+ 'lights': [dev1.entity_id],
+ 'stop_time': '01:00'
+ }
+ })
+ turn_on_calls = mock_service(
+ self.hass, light.DOMAIN, SERVICE_TURN_ON)
+ common.turn_on(self.hass, 'switch.flux')
+ self.hass.block_till_done()
+ fire_time_changed(self.hass, test_time)
+ self.hass.block_till_done()
call = turn_on_calls[-1]
self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 114)
self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.601, 0.382])
@@ -573,23 +579,24 @@ class TestSwitchFlux(unittest.TestCase):
return sunrise_time
return sunset_time
- with patch('homeassistant.util.dt.now', return_value=test_time):
- with patch('homeassistant.helpers.sun.get_astral_event_date',
- side_effect=event_date):
- assert setup_component(self.hass, switch.DOMAIN, {
- switch.DOMAIN: {
- 'platform': 'flux',
- 'name': 'flux',
- 'lights': [dev1.entity_id],
- 'stop_time': '01:00'
- }
- })
- turn_on_calls = mock_service(
- self.hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(self.hass, 'switch.flux')
- self.hass.block_till_done()
- fire_time_changed(self.hass, test_time)
- self.hass.block_till_done()
+ with patch('homeassistant.components.switch.flux.dt_now',
+ return_value=test_time), \
+ patch('homeassistant.helpers.sun.get_astral_event_date',
+ side_effect=event_date):
+ assert setup_component(self.hass, switch.DOMAIN, {
+ switch.DOMAIN: {
+ 'platform': 'flux',
+ 'name': 'flux',
+ 'lights': [dev1.entity_id],
+ 'stop_time': '01:00'
+ }
+ })
+ turn_on_calls = mock_service(
+ self.hass, light.DOMAIN, SERVICE_TURN_ON)
+ common.turn_on(self.hass, 'switch.flux')
+ self.hass.block_till_done()
+ fire_time_changed(self.hass, test_time)
+ self.hass.block_till_done()
call = turn_on_calls[-1]
self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112)
self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379])
@@ -620,25 +627,26 @@ class TestSwitchFlux(unittest.TestCase):
return sunrise_time
return sunset_time
- with patch('homeassistant.util.dt.now', return_value=test_time):
- with patch('homeassistant.helpers.sun.get_astral_event_date',
- side_effect=event_date):
- assert setup_component(self.hass, switch.DOMAIN, {
- switch.DOMAIN: {
- 'platform': 'flux',
- 'name': 'flux',
- 'lights': [dev1.entity_id],
- 'start_colortemp': '1000',
- 'stop_colortemp': '6000',
- 'stop_time': '22:00'
- }
- })
- turn_on_calls = mock_service(
- self.hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(self.hass, 'switch.flux')
- self.hass.block_till_done()
- fire_time_changed(self.hass, test_time)
- self.hass.block_till_done()
+ with patch('homeassistant.components.switch.flux.dt_now',
+ return_value=test_time), \
+ patch('homeassistant.helpers.sun.get_astral_event_date',
+ side_effect=event_date):
+ assert setup_component(self.hass, switch.DOMAIN, {
+ switch.DOMAIN: {
+ 'platform': 'flux',
+ 'name': 'flux',
+ 'lights': [dev1.entity_id],
+ 'start_colortemp': '1000',
+ 'stop_colortemp': '6000',
+ 'stop_time': '22:00'
+ }
+ })
+ turn_on_calls = mock_service(
+ self.hass, light.DOMAIN, SERVICE_TURN_ON)
+ common.turn_on(self.hass, 'switch.flux')
+ self.hass.block_till_done()
+ fire_time_changed(self.hass, test_time)
+ self.hass.block_till_done()
call = turn_on_calls[-1]
self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 159)
self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.469, 0.378])
@@ -669,24 +677,25 @@ class TestSwitchFlux(unittest.TestCase):
return sunrise_time
return sunset_time
- with patch('homeassistant.util.dt.now', return_value=test_time):
- with patch('homeassistant.helpers.sun.get_astral_event_date',
- side_effect=event_date):
- assert setup_component(self.hass, switch.DOMAIN, {
- switch.DOMAIN: {
- 'platform': 'flux',
- 'name': 'flux',
- 'lights': [dev1.entity_id],
- 'brightness': 255,
- 'stop_time': '22:00'
- }
- })
- turn_on_calls = mock_service(
- self.hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(self.hass, 'switch.flux')
- self.hass.block_till_done()
- fire_time_changed(self.hass, test_time)
- self.hass.block_till_done()
+ with patch('homeassistant.components.switch.flux.dt_now',
+ return_value=test_time), \
+ patch('homeassistant.helpers.sun.get_astral_event_date',
+ side_effect=event_date):
+ assert setup_component(self.hass, switch.DOMAIN, {
+ switch.DOMAIN: {
+ 'platform': 'flux',
+ 'name': 'flux',
+ 'lights': [dev1.entity_id],
+ 'brightness': 255,
+ 'stop_time': '22:00'
+ }
+ })
+ turn_on_calls = mock_service(
+ self.hass, light.DOMAIN, SERVICE_TURN_ON)
+ common.turn_on(self.hass, 'switch.flux')
+ self.hass.block_till_done()
+ fire_time_changed(self.hass, test_time)
+ self.hass.block_till_done()
call = turn_on_calls[-1]
self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 255)
self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.506, 0.385])
@@ -731,24 +740,25 @@ class TestSwitchFlux(unittest.TestCase):
print('sunset {}'.format(sunset_time))
return sunset_time
- with patch('homeassistant.util.dt.now', return_value=test_time):
- with patch('homeassistant.helpers.sun.get_astral_event_date',
- side_effect=event_date):
- assert setup_component(self.hass, switch.DOMAIN, {
- switch.DOMAIN: {
- 'platform': 'flux',
- 'name': 'flux',
- 'lights': [dev1.entity_id,
- dev2.entity_id,
- dev3.entity_id]
- }
- })
- turn_on_calls = mock_service(
- self.hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(self.hass, 'switch.flux')
- self.hass.block_till_done()
- fire_time_changed(self.hass, test_time)
- self.hass.block_till_done()
+ with patch('homeassistant.components.switch.flux.dt_now',
+ return_value=test_time), \
+ patch('homeassistant.helpers.sun.get_astral_event_date',
+ side_effect=event_date):
+ assert setup_component(self.hass, switch.DOMAIN, {
+ switch.DOMAIN: {
+ 'platform': 'flux',
+ 'name': 'flux',
+ 'lights': [dev1.entity_id,
+ dev2.entity_id,
+ dev3.entity_id]
+ }
+ })
+ turn_on_calls = mock_service(
+ self.hass, light.DOMAIN, SERVICE_TURN_ON)
+ common.turn_on(self.hass, 'switch.flux')
+ self.hass.block_till_done()
+ fire_time_changed(self.hass, test_time)
+ self.hass.block_till_done()
call = turn_on_calls[-1]
self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 163)
self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.46, 0.376])
@@ -783,23 +793,24 @@ class TestSwitchFlux(unittest.TestCase):
return sunrise_time
return sunset_time
- with patch('homeassistant.util.dt.now', return_value=test_time):
- with patch('homeassistant.helpers.sun.get_astral_event_date',
- side_effect=event_date):
- assert setup_component(self.hass, switch.DOMAIN, {
- switch.DOMAIN: {
- 'platform': 'flux',
- 'name': 'flux',
- 'lights': [dev1.entity_id],
- 'mode': 'mired'
- }
- })
- turn_on_calls = mock_service(
- self.hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(self.hass, 'switch.flux')
- self.hass.block_till_done()
- fire_time_changed(self.hass, test_time)
- self.hass.block_till_done()
+ with patch('homeassistant.components.switch.flux.dt_now',
+ return_value=test_time), \
+ patch('homeassistant.helpers.sun.get_astral_event_date',
+ side_effect=event_date):
+ assert setup_component(self.hass, switch.DOMAIN, {
+ switch.DOMAIN: {
+ 'platform': 'flux',
+ 'name': 'flux',
+ 'lights': [dev1.entity_id],
+ 'mode': 'mired'
+ }
+ })
+ turn_on_calls = mock_service(
+ self.hass, light.DOMAIN, SERVICE_TURN_ON)
+ common.turn_on(self.hass, 'switch.flux')
+ self.hass.block_till_done()
+ fire_time_changed(self.hass, test_time)
+ self.hass.block_till_done()
call = turn_on_calls[-1]
self.assertEqual(call.data[light.ATTR_COLOR_TEMP], 269)
@@ -827,23 +838,24 @@ class TestSwitchFlux(unittest.TestCase):
return sunrise_time
return sunset_time
- with patch('homeassistant.util.dt.now', return_value=test_time):
- with patch('homeassistant.helpers.sun.get_astral_event_date',
- side_effect=event_date):
- assert setup_component(self.hass, switch.DOMAIN, {
- switch.DOMAIN: {
- 'platform': 'flux',
- 'name': 'flux',
- 'lights': [dev1.entity_id],
- 'mode': 'rgb'
- }
- })
- turn_on_calls = mock_service(
- self.hass, light.DOMAIN, SERVICE_TURN_ON)
- common.turn_on(self.hass, 'switch.flux')
- self.hass.block_till_done()
- fire_time_changed(self.hass, test_time)
- self.hass.block_till_done()
+ with patch('homeassistant.components.switch.flux.dt_now',
+ return_value=test_time), \
+ patch('homeassistant.helpers.sun.get_astral_event_date',
+ side_effect=event_date):
+ assert setup_component(self.hass, switch.DOMAIN, {
+ switch.DOMAIN: {
+ 'platform': 'flux',
+ 'name': 'flux',
+ 'lights': [dev1.entity_id],
+ 'mode': 'rgb'
+ }
+ })
+ turn_on_calls = mock_service(
+ self.hass, light.DOMAIN, SERVICE_TURN_ON)
+ common.turn_on(self.hass, 'switch.flux')
+ self.hass.block_till_done()
+ fire_time_changed(self.hass, test_time)
+ self.hass.block_till_done()
call = turn_on_calls[-1]
rgb = (255, 198, 152)
rounded_call = tuple(map(round, call.data[light.ATTR_RGB_COLOR]))
diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py
index 5ad233de284..3552ec0dc2a 100644
--- a/tests/components/switch/test_mqtt.py
+++ b/tests/components/switch/test_mqtt.py
@@ -1,4 +1,5 @@
"""The tests for the MQTT switch platform."""
+import json
import unittest
from unittest.mock import patch
@@ -337,3 +338,42 @@ async def test_discovery_removal_switch(hass, mqtt_mock, caplog):
state = hass.states.get('switch.beer')
assert state is None
+
+
+async def test_entity_device_info_with_identifier(hass, mqtt_mock):
+ """Test MQTT switch device registry integration."""
+ entry = MockConfigEntry(domain=mqtt.DOMAIN)
+ entry.add_to_hass(hass)
+ await async_start(hass, 'homeassistant', {}, entry)
+ registry = await hass.helpers.device_registry.async_get_registry()
+
+ data = json.dumps({
+ 'platform': 'mqtt',
+ 'name': 'Test 1',
+ 'state_topic': 'test-topic',
+ 'command_topic': 'test-command-topic',
+ 'device': {
+ 'identifiers': ['helloworld'],
+ 'connections': [
+ ["mac", "02:5b:26:a8:dc:12"],
+ ],
+ 'manufacturer': 'Whatever',
+ 'name': 'Beer',
+ 'model': 'Glass',
+ 'sw_version': '0.1-beta',
+ },
+ 'unique_id': 'veryunique'
+ })
+ async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config',
+ data)
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({('mqtt', 'helloworld')}, set())
+ assert device is not None
+ assert device.identifiers == {('mqtt', 'helloworld')}
+ assert device.connections == {('mac', "02:5b:26:a8:dc:12")}
+ assert device.manufacturer == 'Whatever'
+ assert device.name == 'Beer'
+ assert device.model == 'Glass'
+ assert device.sw_version == '0.1-beta'
diff --git a/tests/components/switch/test_unifi.py b/tests/components/switch/test_unifi.py
new file mode 100644
index 00000000000..f50bda34883
--- /dev/null
+++ b/tests/components/switch/test_unifi.py
@@ -0,0 +1,345 @@
+"""UniFi POE control platform tests."""
+from collections import deque
+from unittest.mock import Mock
+
+import pytest
+
+import aiounifi
+from aiounifi.clients import Clients
+from aiounifi.devices import Devices
+
+from homeassistant import config_entries
+from homeassistant.components import unifi
+from homeassistant.setup import async_setup_component
+
+import homeassistant.components.switch as switch
+
+from tests.common import mock_coro
+
+CLIENT_1 = {
+ 'hostname': 'client_1',
+ 'ip': '10.0.0.1',
+ 'is_wired': True,
+ 'mac': '00:00:00:00:00:01',
+ 'name': 'POE Client 1',
+ 'oui': 'Producer',
+ 'sw_mac': '00:00:00:00:01:01',
+ 'sw_port': 1,
+ 'wired-rx_bytes': 1234000000,
+ 'wired-tx_bytes': 5678000000
+}
+CLIENT_2 = {
+ 'hostname': 'client_2',
+ 'ip': '10.0.0.2',
+ 'is_wired': True,
+ 'mac': '00:00:00:00:00:02',
+ 'name': 'POE Client 2',
+ 'oui': 'Producer',
+ 'sw_mac': '00:00:00:00:01:01',
+ 'sw_port': 2,
+ 'wired-rx_bytes': 1234000000,
+ 'wired-tx_bytes': 5678000000
+}
+CLIENT_3 = {
+ 'hostname': 'client_3',
+ 'ip': '10.0.0.3',
+ 'is_wired': True,
+ 'mac': '00:00:00:00:00:03',
+ 'name': 'Non-POE Client 3',
+ 'oui': 'Producer',
+ 'sw_mac': '00:00:00:00:01:01',
+ 'sw_port': 3,
+ 'wired-rx_bytes': 1234000000,
+ 'wired-tx_bytes': 5678000000
+}
+CLIENT_4 = {
+ 'hostname': 'client_4',
+ 'ip': '10.0.0.4',
+ 'is_wired': True,
+ 'mac': '00:00:00:00:00:04',
+ 'name': 'Non-POE Client 4',
+ 'oui': 'Producer',
+ 'sw_mac': '00:00:00:00:01:01',
+ 'sw_port': 4,
+ 'wired-rx_bytes': 1234000000,
+ 'wired-tx_bytes': 5678000000
+}
+POE_SWITCH_CLIENTS = [
+ {
+ 'hostname': 'client_1',
+ 'ip': '10.0.0.1',
+ 'is_wired': True,
+ 'mac': '00:00:00:00:00:01',
+ 'name': 'POE Client 1',
+ 'oui': 'Producer',
+ 'sw_mac': '00:00:00:00:01:01',
+ 'sw_port': 1,
+ 'wired-rx_bytes': 1234000000,
+ 'wired-tx_bytes': 5678000000
+ },
+ {
+ 'hostname': 'client_2',
+ 'ip': '10.0.0.2',
+ 'is_wired': True,
+ 'mac': '00:00:00:00:00:02',
+ 'name': 'POE Client 2',
+ 'oui': 'Producer',
+ 'sw_mac': '00:00:00:00:01:01',
+ 'sw_port': 1,
+ 'wired-rx_bytes': 1234000000,
+ 'wired-tx_bytes': 5678000000
+ }
+]
+
+DEVICE_1 = {
+ 'device_id': 'mock-id',
+ 'ip': '10.0.1.1',
+ 'mac': '00:00:00:00:01:01',
+ 'type': 'usw',
+ 'name': 'mock-name',
+ 'portconf_id': '',
+ 'port_table': [
+ {
+ 'media': 'GE',
+ 'name': 'Port 1',
+ 'port_idx': 1,
+ 'poe_class': 'Class 4',
+ 'poe_enable': True,
+ 'poe_mode': 'auto',
+ 'poe_power': '2.56',
+ 'poe_voltage': '53.40',
+ 'portconf_id': '1a1',
+ 'port_poe': True,
+ 'up': True
+ },
+ {
+ 'media': 'GE',
+ 'name': 'Port 2',
+ 'port_idx': 2,
+ 'poe_class': 'Class 4',
+ 'poe_enable': True,
+ 'poe_mode': 'auto',
+ 'poe_power': '2.56',
+ 'poe_voltage': '53.40',
+ 'portconf_id': '1a2',
+ 'port_poe': True,
+ 'up': True
+ },
+ {
+ 'media': 'GE',
+ 'name': 'Port 3',
+ 'port_idx': 3,
+ 'poe_class': 'Unknown',
+ 'poe_enable': False,
+ 'poe_mode': 'off',
+ 'poe_power': '0.00',
+ 'poe_voltage': '0.00',
+ 'portconf_id': '1a3',
+ 'port_poe': False,
+ 'up': True
+ },
+ {
+ 'media': 'GE',
+ 'name': 'Port 4',
+ 'port_idx': 4,
+ 'poe_class': 'Unknown',
+ 'poe_enable': False,
+ 'poe_mode': 'auto',
+ 'poe_power': '0.00',
+ 'poe_voltage': '0.00',
+ 'portconf_id': '1a4',
+ 'port_poe': True,
+ 'up': True
+ }
+ ]
+}
+
+CONTROLLER_DATA = {
+ unifi.CONF_HOST: 'mock-host',
+ unifi.CONF_USERNAME: 'mock-user',
+ unifi.CONF_PASSWORD: 'mock-pswd',
+ unifi.CONF_PORT: 1234,
+ unifi.CONF_SITE_ID: 'mock-site',
+ unifi.CONF_VERIFY_SSL: True
+}
+
+ENTRY_CONFIG = {
+ unifi.CONF_CONTROLLER: CONTROLLER_DATA,
+ unifi.CONF_POE_CONTROL: True
+}
+
+CONTROLLER_ID = unifi.CONTROLLER_ID.format(host='mock-host', site='mock-site')
+
+
+@pytest.fixture
+def mock_controller(hass):
+ """Mock a UniFi Controller."""
+ controller = Mock(
+ available=True,
+ api=Mock(),
+ spec=unifi.UniFiController
+ )
+ controller.mock_requests = []
+
+ controller.mock_client_responses = deque()
+ controller.mock_device_responses = deque()
+
+ async def mock_request(method, path, **kwargs):
+ kwargs['method'] = method
+ kwargs['path'] = path
+ controller.mock_requests.append(kwargs)
+ if path == 's/{site}/stat/sta':
+ return controller.mock_client_responses.popleft()
+ if path == 's/{site}/stat/device':
+ return controller.mock_device_responses.popleft()
+ return None
+
+ controller.api.clients = Clients({}, mock_request)
+ controller.api.devices = Devices({}, mock_request)
+
+ return controller
+
+
+async def setup_controller(hass, mock_controller):
+ """Load the UniFi switch platform with the provided controller."""
+ hass.config.components.add(unifi.DOMAIN)
+ hass.data[unifi.DOMAIN] = {CONTROLLER_ID: mock_controller}
+ config_entry = config_entries.ConfigEntry(
+ 1, unifi.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test',
+ config_entries.CONN_CLASS_LOCAL_POLL)
+ await hass.config_entries.async_forward_entry_setup(config_entry, 'switch')
+ # To flush out the service call to update the group
+ await hass.async_block_till_done()
+
+
+async def test_platform_manually_configured(hass):
+ """Test that we do not discover anything or try to set up a bridge."""
+ assert await async_setup_component(hass, switch.DOMAIN, {
+ 'switch': {
+ 'platform': 'unifi'
+ }
+ }) is True
+ assert unifi.DOMAIN not in hass.data
+
+
+async def test_no_clients(hass, mock_controller):
+ """Test the update_clients function when no clients are found."""
+ mock_controller.mock_client_responses.append({})
+ await setup_controller(hass, mock_controller)
+ assert len(mock_controller.mock_requests) == 2
+ assert not hass.states.async_all()
+
+
+async def test_switches(hass, mock_controller):
+ """Test the update_items function with some lights."""
+ mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_4])
+ mock_controller.mock_device_responses.append([DEVICE_1])
+ await setup_controller(hass, mock_controller)
+ assert len(mock_controller.mock_requests) == 2
+ # 1 All Lights group, 2 lights
+ assert len(hass.states.async_all()) == 2
+
+ switch_1 = hass.states.get('switch.client_1')
+ assert switch_1 is not None
+ assert switch_1.state == 'on'
+ assert switch_1.attributes['power'] == '2.56'
+ assert switch_1.attributes['received'] == 1234
+ assert switch_1.attributes['sent'] == 5678
+ assert switch_1.attributes['switch'] == '00:00:00:00:01:01'
+ assert switch_1.attributes['port'] == 1
+ assert switch_1.attributes['poe_mode'] == 'auto'
+
+ switch = hass.states.get('switch.client_4')
+ assert switch is None
+
+
+async def test_new_client_discovered(hass, mock_controller):
+ """Test if 2nd update has a new client."""
+ mock_controller.mock_client_responses.append([CLIENT_1])
+ mock_controller.mock_device_responses.append([DEVICE_1])
+
+ await setup_controller(hass, mock_controller)
+ assert len(mock_controller.mock_requests) == 2
+ assert len(hass.states.async_all()) == 2
+
+ mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2])
+ mock_controller.mock_device_responses.append([DEVICE_1])
+
+ # Calling a service will trigger the updates to run
+ await hass.services.async_call('switch', 'turn_off', {
+ 'entity_id': 'switch.client_1'
+ }, blocking=True)
+ # 2x light update, 1 turn on request
+ assert len(mock_controller.mock_requests) == 5
+ assert len(hass.states.async_all()) == 3
+
+ switch = hass.states.get('switch.client_2')
+ assert switch is not None
+ assert switch.state == 'on'
+
+
+async def test_failed_update_successful_login(hass, mock_controller):
+ """Running update can login when requested."""
+ mock_controller.available = False
+ mock_controller.api.clients.update = Mock()
+ mock_controller.api.clients.update.side_effect = aiounifi.LoginRequired
+ mock_controller.api.login = Mock()
+ mock_controller.api.login.return_value = mock_coro()
+
+ await setup_controller(hass, mock_controller)
+ assert len(mock_controller.mock_requests) == 0
+
+ assert mock_controller.available is True
+
+
+async def test_failed_update_failed_login(hass, mock_controller):
+ """Running update can handle a failed login."""
+ mock_controller.api.clients.update = Mock()
+ mock_controller.api.clients.update.side_effect = aiounifi.LoginRequired
+ mock_controller.api.login = Mock()
+ mock_controller.api.login.side_effect = aiounifi.AiounifiException
+
+ await setup_controller(hass, mock_controller)
+ assert len(mock_controller.mock_requests) == 0
+
+ assert mock_controller.available is False
+
+
+async def test_failed_update_unreachable_controller(hass, mock_controller):
+ """Running update can handle a unreachable controller."""
+ mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2])
+ mock_controller.mock_device_responses.append([DEVICE_1])
+
+ await setup_controller(hass, mock_controller)
+
+ mock_controller.api.clients.update = Mock()
+ mock_controller.api.clients.update.side_effect = aiounifi.AiounifiException
+
+ # Calling a service will trigger the updates to run
+ await hass.services.async_call('switch', 'turn_off', {
+ 'entity_id': 'switch.client_1'
+ }, blocking=True)
+ # 2x light update, 1 turn on request
+ assert len(mock_controller.mock_requests) == 3
+ assert len(hass.states.async_all()) == 3
+
+ assert mock_controller.available is False
+
+
+async def test_ignore_multiple_poe_clients_on_same_port(hass, mock_controller):
+ """Ignore when there are multiple POE driven clients on same port.
+
+ If there is a non-UniFi switch powered by POE,
+ clients will be transparently marked as having POE as well.
+ """
+ mock_controller.mock_client_responses.append(POE_SWITCH_CLIENTS)
+ mock_controller.mock_device_responses.append([DEVICE_1])
+ await setup_controller(hass, mock_controller)
+ assert len(mock_controller.mock_requests) == 2
+ # 1 All Lights group, 2 lights
+ assert len(hass.states.async_all()) == 0
+
+ switch_1 = hass.states.get('switch.client_1')
+ switch_2 = hass.states.get('switch.client_2')
+ assert switch_1 is None
+ assert switch_2 is None
diff --git a/tests/components/test_dyson.py b/tests/components/test_dyson.py
index 19c39754eb2..0352551aec9 100644
--- a/tests/components/test_dyson.py
+++ b/tests/components/test_dyson.py
@@ -87,7 +87,7 @@ class DysonTest(unittest.TestCase):
self.assertEqual(mocked_login.call_count, 1)
self.assertEqual(mocked_devices.call_count, 1)
self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1)
- self.assertEqual(mocked_discovery.call_count, 3)
+ self.assertEqual(mocked_discovery.call_count, 4)
@mock.patch('libpurecoollink.dyson.DysonAccount.devices',
return_value=[_get_dyson_account_device_not_available()])
@@ -172,7 +172,7 @@ class DysonTest(unittest.TestCase):
self.assertEqual(mocked_login.call_count, 1)
self.assertEqual(mocked_devices.call_count, 1)
self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1)
- self.assertEqual(mocked_discovery.call_count, 3)
+ self.assertEqual(mocked_discovery.call_count, 4)
@mock.patch('libpurecoollink.dyson.DysonAccount.devices',
return_value=[_get_dyson_account_device_not_available()])
diff --git a/tests/components/test_init.py b/tests/components/test_init.py
index 68396f5abcb..b9152bbdd6a 100644
--- a/tests/components/test_init.py
+++ b/tests/components/test_init.py
@@ -355,3 +355,17 @@ async def test_turn_on_to_not_block_for_domains_without_service(hass):
'light', 'turn_on', {'entity_id': ['light.bla', 'light.test']}, True)
assert mock_call.call_args_list[1][0] == (
'sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False)
+
+
+async def test_entity_update(hass):
+ """Test being able to call entity update."""
+ await comps.async_setup(hass, {})
+
+ with patch('homeassistant.helpers.entity_component.async_update_entity',
+ return_value=mock_coro()) as mock_update:
+ await hass.services.async_call('homeassistant', 'update_entity', {
+ 'entity_id': 'light.kitchen'
+ }, blocking=True)
+
+ assert len(mock_update.mock_calls) == 1
+ assert mock_update.mock_calls[0][1][1] == 'light.kitchen'
diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py
index 3bb3ae57c68..1100a16b381 100644
--- a/tests/components/test_logbook.py
+++ b/tests/components/test_logbook.py
@@ -7,11 +7,15 @@ import unittest
from homeassistant.components import sun
import homeassistant.core as ha
from homeassistant.const import (
+ ATTR_ENTITY_ID, ATTR_SERVICE,
EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
ATTR_HIDDEN, STATE_NOT_HOME, STATE_ON, STATE_OFF)
import homeassistant.util.dt as dt_util
from homeassistant.components import logbook, recorder
from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME
+from homeassistant.components.homekit.const import (
+ ATTR_DISPLAY_NAME, ATTR_VALUE, DOMAIN as DOMAIN_HOMEKIT,
+ EVENT_HOMEKIT_CHANGED)
from homeassistant.setup import setup_component, async_setup_component
from tests.common import (
@@ -684,3 +688,31 @@ async def test_humanify_alexa_event(hass):
assert event3['message'] == \
'send command Alexa.PowerController/TurnOn for light.non_existing'
assert event3['entity_id'] == 'light.non_existing'
+
+
+async def test_humanify_homekit_changed_event(hass):
+ """Test humanifying HomeKit changed event."""
+ event1, event2 = list(logbook.humanify(hass, [
+ ha.Event(EVENT_HOMEKIT_CHANGED, {
+ ATTR_ENTITY_ID: 'lock.front_door',
+ ATTR_DISPLAY_NAME: 'Front Door',
+ ATTR_SERVICE: 'lock',
+ }),
+ ha.Event(EVENT_HOMEKIT_CHANGED, {
+ ATTR_ENTITY_ID: 'cover.window',
+ ATTR_DISPLAY_NAME: 'Window',
+ ATTR_SERVICE: 'set_cover_position',
+ ATTR_VALUE: 75,
+ }),
+ ]))
+
+ assert event1['name'] == 'HomeKit'
+ assert event1['domain'] == DOMAIN_HOMEKIT
+ assert event1['message'] == 'send command lock for Front Door'
+ assert event1['entity_id'] == 'lock.front_door'
+
+ assert event2['name'] == 'HomeKit'
+ assert event1['domain'] == DOMAIN_HOMEKIT
+ assert event2['message'] == \
+ 'send command set_cover_position to 75 for Window'
+ assert event2['entity_id'] == 'cover.window'
diff --git a/tests/components/unifi/__init__.py b/tests/components/unifi/__init__.py
new file mode 100644
index 00000000000..e75b2778d2b
--- /dev/null
+++ b/tests/components/unifi/__init__.py
@@ -0,0 +1 @@
+"""Tests for the UniFi component."""
diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py
new file mode 100644
index 00000000000..b3b222d902a
--- /dev/null
+++ b/tests/components/unifi/test_controller.py
@@ -0,0 +1,266 @@
+"""Test UniFi Controller."""
+from unittest.mock import Mock, patch
+
+from homeassistant.components import unifi
+from homeassistant.components.unifi import controller, errors
+
+from tests.common import mock_coro
+
+CONTROLLER_DATA = {
+ unifi.CONF_HOST: '1.2.3.4',
+ unifi.CONF_USERNAME: 'username',
+ unifi.CONF_PASSWORD: 'password',
+ unifi.CONF_PORT: 1234,
+ unifi.CONF_SITE_ID: 'site',
+ unifi.CONF_VERIFY_SSL: True
+}
+
+ENTRY_CONFIG = {
+ unifi.CONF_CONTROLLER: CONTROLLER_DATA,
+ unifi.CONF_POE_CONTROL: True
+ }
+
+
+async def test_controller_setup():
+ """Successful setup."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = ENTRY_CONFIG
+ api = Mock()
+ api.initialize.return_value = mock_coro(True)
+
+ unifi_controller = controller.UniFiController(hass, entry)
+
+ with patch.object(controller, 'get_controller',
+ return_value=mock_coro(api)):
+ assert await unifi_controller.async_setup() is True
+
+ assert unifi_controller.api is api
+ assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1
+ assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \
+ (entry, 'switch')
+
+
+async def test_controller_host():
+ """Config entry host and controller host are the same."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = ENTRY_CONFIG
+
+ unifi_controller = controller.UniFiController(hass, entry)
+
+ assert unifi_controller.host == '1.2.3.4'
+
+
+async def test_controller_mac():
+ """Test that it is possible to identify controller mac."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = ENTRY_CONFIG
+ client = Mock()
+ client.ip = '1.2.3.4'
+ client.mac = '00:11:22:33:44:55'
+ api = Mock()
+ api.initialize.return_value = mock_coro(True)
+ api.clients = {'client1': client}
+
+ unifi_controller = controller.UniFiController(hass, entry)
+
+ with patch.object(controller, 'get_controller',
+ return_value=mock_coro(api)):
+ assert await unifi_controller.async_setup() is True
+
+ assert unifi_controller.mac == '00:11:22:33:44:55'
+
+
+async def test_controller_no_mac():
+ """Test that it works to not find the controllers mac."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = ENTRY_CONFIG
+ client = Mock()
+ client.ip = '5.6.7.8'
+ api = Mock()
+ api.initialize.return_value = mock_coro(True)
+ api.clients = {'client1': client}
+
+ unifi_controller = controller.UniFiController(hass, entry)
+
+ with patch.object(controller, 'get_controller',
+ return_value=mock_coro(api)):
+ assert await unifi_controller.async_setup() is True
+
+ assert unifi_controller.mac is None
+
+
+async def test_controller_not_accessible():
+ """Retry to login gets scheduled when connection fails."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = ENTRY_CONFIG
+ api = Mock()
+ api.initialize.return_value = mock_coro(True)
+
+ unifi_controller = controller.UniFiController(hass, entry)
+
+ with patch.object(controller, 'get_controller',
+ side_effect=errors.CannotConnect):
+ assert await unifi_controller.async_setup() is False
+
+ assert len(hass.helpers.event.async_call_later.mock_calls) == 1
+ # Assert we are going to wait 2 seconds
+ assert hass.helpers.event.async_call_later.mock_calls[0][1][0] == 2
+
+
+async def test_controller_unknown_error():
+ """Unknown errors are handled."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = ENTRY_CONFIG
+ api = Mock()
+ api.initialize.return_value = mock_coro(True)
+
+ unifi_controller = controller.UniFiController(hass, entry)
+
+ with patch.object(controller, 'get_controller', side_effect=Exception):
+ assert await unifi_controller.async_setup() is False
+
+ assert not hass.helpers.event.async_call_later.mock_calls
+
+
+async def test_reset_cancels_retry_setup():
+ """Resetting a controller while we're waiting to retry setup."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = ENTRY_CONFIG
+
+ unifi_controller = controller.UniFiController(hass, entry)
+
+ with patch.object(controller, 'get_controller',
+ side_effect=errors.CannotConnect):
+ assert await unifi_controller.async_setup() is False
+
+ mock_call_later = hass.helpers.event.async_call_later
+
+ assert len(mock_call_later.mock_calls) == 1
+
+ assert await unifi_controller.async_reset()
+
+ assert len(mock_call_later.mock_calls) == 2
+ assert len(mock_call_later.return_value.mock_calls) == 1
+
+
+async def test_reset_if_entry_had_wrong_auth():
+ """Calling reset when the entry contains wrong auth."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = ENTRY_CONFIG
+
+ unifi_controller = controller.UniFiController(hass, entry)
+
+ with patch.object(controller, 'get_controller',
+ side_effect=errors.AuthenticationRequired):
+ assert await unifi_controller.async_setup() is False
+
+ assert not hass.async_add_job.mock_calls
+
+ assert await unifi_controller.async_reset()
+
+
+async def test_reset_unloads_entry_if_setup():
+ """Calling reset when the entry has been setup."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = ENTRY_CONFIG
+ api = Mock()
+ api.initialize.return_value = mock_coro(True)
+
+ unifi_controller = controller.UniFiController(hass, entry)
+
+ with patch.object(controller, 'get_controller',
+ return_value=mock_coro(api)):
+ assert await unifi_controller.async_setup() is True
+
+ assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1
+
+ hass.config_entries.async_forward_entry_unload.return_value = \
+ mock_coro(True)
+ assert await unifi_controller.async_reset()
+
+ assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 1
+
+
+async def test_reset_unloads_entry_without_poe_control():
+ """Calling reset while the entry has been setup."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = dict(ENTRY_CONFIG)
+ entry.data[unifi.CONF_POE_CONTROL] = False
+ api = Mock()
+ api.initialize.return_value = mock_coro(True)
+
+ unifi_controller = controller.UniFiController(hass, entry)
+
+ with patch.object(controller, 'get_controller',
+ return_value=mock_coro(api)):
+ assert await unifi_controller.async_setup() is True
+
+ assert not hass.config_entries.async_forward_entry_setup.mock_calls
+
+ hass.config_entries.async_forward_entry_unload.return_value = \
+ mock_coro(True)
+ assert await unifi_controller.async_reset()
+
+ assert not hass.config_entries.async_forward_entry_unload.mock_calls
+
+
+async def test_get_controller(hass):
+ """Successful call."""
+ with patch('aiounifi.Controller.login', return_value=mock_coro()):
+ assert await controller.get_controller(hass, **CONTROLLER_DATA)
+
+
+async def test_get_controller_verify_ssl_false(hass):
+ """Successful call with verify ssl set to false."""
+ controller_data = dict(CONTROLLER_DATA)
+ controller_data[unifi.CONF_VERIFY_SSL] = False
+ with patch('aiounifi.Controller.login', return_value=mock_coro()):
+ assert await controller.get_controller(hass, **controller_data)
+
+
+async def test_get_controller_login_failed(hass):
+ """Check that get_controller can handle a failed login."""
+ import aiounifi
+ result = None
+ with patch('aiounifi.Controller.login', side_effect=aiounifi.Unauthorized):
+ try:
+ result = await controller.get_controller(hass, **CONTROLLER_DATA)
+ except errors.AuthenticationRequired:
+ pass
+ assert result is None
+
+
+async def test_get_controller_controller_unavailable(hass):
+ """Check that get_controller can handle controller being unavailable."""
+ import aiounifi
+ result = None
+ with patch('aiounifi.Controller.login',
+ side_effect=aiounifi.RequestError):
+ try:
+ result = await controller.get_controller(hass, **CONTROLLER_DATA)
+ except errors.CannotConnect:
+ pass
+ assert result is None
+
+
+async def test_get_controller_unknown_error(hass):
+ """Check that get_controller can handle unkown errors."""
+ import aiounifi
+ result = None
+ with patch('aiounifi.Controller.login',
+ side_effect=aiounifi.AiounifiException):
+ try:
+ result = await controller.get_controller(hass, **CONTROLLER_DATA)
+ except errors.AuthenticationRequired:
+ pass
+ assert result is None
diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py
new file mode 100644
index 00000000000..400dd3fd93e
--- /dev/null
+++ b/tests/components/unifi/test_init.py
@@ -0,0 +1,330 @@
+"""Test UniFi setup process."""
+from unittest.mock import Mock, patch
+
+from homeassistant.components import unifi
+from homeassistant.setup import async_setup_component
+
+from tests.common import mock_coro, MockConfigEntry
+
+
+async def test_setup_with_no_config(hass):
+ """Test that we do not discover anything or try to set up a bridge."""
+ assert await async_setup_component(hass, unifi.DOMAIN, {}) is True
+ assert unifi.DOMAIN not in hass.data
+
+
+async def test_successful_config_entry(hass):
+ """Test that configured options for a host are loaded via config entry."""
+ entry = MockConfigEntry(domain=unifi.DOMAIN, data={
+ 'controller': {
+ 'host': '0.0.0.0',
+ 'username': 'user',
+ 'password': 'pass',
+ 'port': 80,
+ 'site': 'default',
+ 'verify_ssl': True
+ },
+ 'poe_control': True
+ })
+ entry.add_to_hass(hass)
+ mock_registry = Mock()
+ with patch.object(unifi, 'UniFiController') as mock_controller, \
+ patch('homeassistant.helpers.device_registry.async_get_registry',
+ return_value=mock_coro(mock_registry)):
+ mock_controller.return_value.async_setup.return_value = mock_coro(True)
+ mock_controller.return_value.mac = '00:11:22:33:44:55'
+ assert await unifi.async_setup_entry(hass, entry) is True
+
+ assert len(mock_controller.mock_calls) == 2
+ p_hass, p_entry = mock_controller.mock_calls[0][1]
+
+ assert p_hass is hass
+ assert p_entry is entry
+
+ assert len(mock_registry.mock_calls) == 1
+ assert mock_registry.mock_calls[0][2] == {
+ 'config_entry_id': entry.entry_id,
+ 'connections': {
+ ('mac', '00:11:22:33:44:55')
+ },
+ 'manufacturer': 'Ubiquiti',
+ 'model': "UniFi Controller",
+ 'name': "UniFi Controller",
+ }
+
+
+async def test_controller_fail_setup(hass):
+ """Test that configured options for a host are loaded via config entry."""
+ entry = MockConfigEntry(domain=unifi.DOMAIN, data={
+ 'controller': {
+ 'host': '0.0.0.0',
+ 'username': 'user',
+ 'password': 'pass',
+ 'port': 80,
+ 'site': 'default',
+ 'verify_ssl': True
+ },
+ 'poe_control': True
+ })
+ entry.add_to_hass(hass)
+
+ with patch.object(unifi, 'UniFiController') as mock_cntrlr:
+ mock_cntrlr.return_value.async_setup.return_value = mock_coro(False)
+ assert await unifi.async_setup_entry(hass, entry) is False
+
+ controller_id = unifi.CONTROLLER_ID.format(
+ host='0.0.0.0', site='default'
+ )
+ assert controller_id not in hass.data[unifi.DOMAIN]
+
+
+async def test_controller_no_mac(hass):
+ """Test that configured options for a host are loaded via config entry."""
+ entry = MockConfigEntry(domain=unifi.DOMAIN, data={
+ 'controller': {
+ 'host': '0.0.0.0',
+ 'username': 'user',
+ 'password': 'pass',
+ 'port': 80,
+ 'site': 'default',
+ 'verify_ssl': True
+ },
+ 'poe_control': True
+ })
+ entry.add_to_hass(hass)
+ mock_registry = Mock()
+ with patch.object(unifi, 'UniFiController') as mock_controller, \
+ patch('homeassistant.helpers.device_registry.async_get_registry',
+ return_value=mock_coro(mock_registry)):
+ mock_controller.return_value.async_setup.return_value = mock_coro(True)
+ mock_controller.return_value.mac = None
+ assert await unifi.async_setup_entry(hass, entry) is True
+
+ assert len(mock_controller.mock_calls) == 2
+
+ assert len(mock_registry.mock_calls) == 0
+
+
+async def test_unload_entry(hass):
+ """Test being able to unload an entry."""
+ entry = MockConfigEntry(domain=unifi.DOMAIN, data={
+ 'controller': {
+ 'host': '0.0.0.0',
+ 'username': 'user',
+ 'password': 'pass',
+ 'port': 80,
+ 'site': 'default',
+ 'verify_ssl': True
+ },
+ 'poe_control': True
+ })
+ entry.add_to_hass(hass)
+
+ with patch.object(unifi, 'UniFiController') as mock_controller, \
+ patch('homeassistant.helpers.device_registry.async_get_registry',
+ return_value=mock_coro(Mock())):
+ mock_controller.return_value.async_setup.return_value = mock_coro(True)
+ mock_controller.return_value.mac = '00:11:22:33:44:55'
+ assert await unifi.async_setup_entry(hass, entry) is True
+
+ assert len(mock_controller.return_value.mock_calls) == 1
+
+ mock_controller.return_value.async_reset.return_value = mock_coro(True)
+ assert await unifi.async_unload_entry(hass, entry)
+ assert len(mock_controller.return_value.async_reset.mock_calls) == 1
+ assert hass.data[unifi.DOMAIN] == {}
+
+
+async def test_flow_works(hass, aioclient_mock):
+ """Test config flow."""
+ flow = unifi.UnifiFlowHandler()
+ flow.hass = hass
+
+ with patch('aiounifi.Controller') as mock_controller:
+ def mock_constructor(host, username, password, port, site, websession):
+ """Fake the controller constructor."""
+ mock_controller.host = host
+ mock_controller.username = username
+ mock_controller.password = password
+ mock_controller.port = port
+ mock_controller.site = site
+ return mock_controller
+
+ mock_controller.side_effect = mock_constructor
+ mock_controller.login.return_value = mock_coro()
+ mock_controller.sites.return_value = mock_coro({
+ 'site1': {'name': 'default', 'role': 'admin', 'desc': 'site name'}
+ })
+
+ await flow.async_step_user(user_input={
+ unifi.CONF_HOST: '1.2.3.4',
+ unifi.CONF_USERNAME: 'username',
+ unifi.CONF_PASSWORD: 'password',
+ unifi.CONF_PORT: 1234,
+ unifi.CONF_VERIFY_SSL: True
+ })
+
+ result = await flow.async_step_site(user_input={})
+
+ assert mock_controller.host == '1.2.3.4'
+ assert len(mock_controller.login.mock_calls) == 1
+ assert len(mock_controller.sites.mock_calls) == 1
+
+ assert result['type'] == 'create_entry'
+ assert result['title'] == 'site name'
+ assert result['data'] == {
+ unifi.CONF_CONTROLLER: {
+ unifi.CONF_HOST: '1.2.3.4',
+ unifi.CONF_USERNAME: 'username',
+ unifi.CONF_PASSWORD: 'password',
+ unifi.CONF_PORT: 1234,
+ unifi.CONF_SITE_ID: 'default',
+ unifi.CONF_VERIFY_SSL: True
+ },
+ unifi.CONF_POE_CONTROL: True
+ }
+
+
+async def test_controller_multiple_sites(hass):
+ """Test config flow."""
+ flow = unifi.UnifiFlowHandler()
+ flow.hass = hass
+
+ flow.config = {
+ unifi.CONF_HOST: '1.2.3.4',
+ unifi.CONF_USERNAME: 'username',
+ unifi.CONF_PASSWORD: 'password',
+ }
+ flow.sites = {
+ 'site1': {
+ 'name': 'default', 'role': 'admin', 'desc': 'site name'
+ },
+ 'site2': {
+ 'name': 'site2', 'role': 'admin', 'desc': 'site2 name'
+ }
+ }
+
+ result = await flow.async_step_site()
+
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'site'
+
+ assert result['data_schema']({'site': 'site name'})
+ assert result['data_schema']({'site': 'site2 name'})
+
+
+async def test_controller_site_already_configured(hass):
+ """Test config flow."""
+ flow = unifi.UnifiFlowHandler()
+ flow.hass = hass
+
+ entry = MockConfigEntry(domain=unifi.DOMAIN, data={
+ 'controller': {
+ 'host': '1.2.3.4',
+ 'site': 'default',
+ }
+ })
+ entry.add_to_hass(hass)
+
+ flow.config = {
+ unifi.CONF_HOST: '1.2.3.4',
+ unifi.CONF_USERNAME: 'username',
+ unifi.CONF_PASSWORD: 'password',
+ }
+ flow.desc = 'site name'
+ flow.sites = {
+ 'site1': {
+ 'name': 'default', 'role': 'admin', 'desc': 'site name'
+ }
+ }
+
+ result = await flow.async_step_site()
+
+ assert result['type'] == 'abort'
+
+
+async def test_user_permissions_low(hass, aioclient_mock):
+ """Test config flow."""
+ flow = unifi.UnifiFlowHandler()
+ flow.hass = hass
+
+ with patch('aiounifi.Controller') as mock_controller:
+ def mock_constructor(host, username, password, port, site, websession):
+ """Fake the controller constructor."""
+ mock_controller.host = host
+ mock_controller.username = username
+ mock_controller.password = password
+ mock_controller.port = port
+ mock_controller.site = site
+ return mock_controller
+
+ mock_controller.side_effect = mock_constructor
+ mock_controller.login.return_value = mock_coro()
+ mock_controller.sites.return_value = mock_coro({
+ 'site1': {'name': 'default', 'role': 'viewer', 'desc': 'site name'}
+ })
+
+ await flow.async_step_user(user_input={
+ unifi.CONF_HOST: '1.2.3.4',
+ unifi.CONF_USERNAME: 'username',
+ unifi.CONF_PASSWORD: 'password',
+ unifi.CONF_PORT: 1234,
+ unifi.CONF_VERIFY_SSL: True
+ })
+
+ result = await flow.async_step_site(user_input={})
+
+ assert result['type'] == 'abort'
+
+
+async def test_user_credentials_faulty(hass, aioclient_mock):
+ """Test config flow."""
+ flow = unifi.UnifiFlowHandler()
+ flow.hass = hass
+
+ with patch.object(unifi, 'get_controller',
+ side_effect=unifi.errors.AuthenticationRequired):
+ result = await flow.async_step_user({
+ unifi.CONF_HOST: '1.2.3.4',
+ unifi.CONF_USERNAME: 'username',
+ unifi.CONF_PASSWORD: 'password',
+ unifi.CONF_SITE_ID: 'default',
+ })
+
+ assert result['type'] == 'form'
+ assert result['errors'] == {'base': 'faulty_credentials'}
+
+
+async def test_controller_is_unavailable(hass, aioclient_mock):
+ """Test config flow."""
+ flow = unifi.UnifiFlowHandler()
+ flow.hass = hass
+
+ with patch.object(unifi, 'get_controller',
+ side_effect=unifi.errors.CannotConnect):
+ result = await flow.async_step_user({
+ unifi.CONF_HOST: '1.2.3.4',
+ unifi.CONF_USERNAME: 'username',
+ unifi.CONF_PASSWORD: 'password',
+ unifi.CONF_SITE_ID: 'default',
+ })
+
+ assert result['type'] == 'form'
+ assert result['errors'] == {'base': 'service_unavailable'}
+
+
+async def test_controller_unkown_problem(hass, aioclient_mock):
+ """Test config flow."""
+ flow = unifi.UnifiFlowHandler()
+ flow.hass = hass
+
+ with patch.object(unifi, 'get_controller',
+ side_effect=Exception):
+ result = await flow.async_step_user({
+ unifi.CONF_HOST: '1.2.3.4',
+ unifi.CONF_USERNAME: 'username',
+ unifi.CONF_PASSWORD: 'password',
+ unifi.CONF_SITE_ID: 'default',
+ })
+
+ assert result['type'] == 'abort'
diff --git a/tests/components/water_heater/__init__.py b/tests/components/water_heater/__init__.py
new file mode 100644
index 00000000000..673119bf16e
--- /dev/null
+++ b/tests/components/water_heater/__init__.py
@@ -0,0 +1 @@
+"""The tests for water heater component."""
diff --git a/tests/components/water_heater/common.py b/tests/components/water_heater/common.py
new file mode 100644
index 00000000000..34173e7f110
--- /dev/null
+++ b/tests/components/water_heater/common.py
@@ -0,0 +1,51 @@
+"""Collection of helper methods.
+
+All containing methods are legacy helpers that should not be used by new
+components. Instead call the service directly.
+"""
+from homeassistant.components.water_heater import (
+ _LOGGER, ATTR_AWAY_MODE,
+ ATTR_OPERATION_MODE, DOMAIN, SERVICE_SET_AWAY_MODE,
+ SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE)
+from homeassistant.const import (
+ ATTR_ENTITY_ID, ATTR_TEMPERATURE)
+from homeassistant.loader import bind_hass
+
+
+@bind_hass
+def set_away_mode(hass, away_mode, entity_id=None):
+ """Turn all or specified water_heater devices away mode on."""
+ data = {
+ ATTR_AWAY_MODE: away_mode
+ }
+
+ if entity_id:
+ data[ATTR_ENTITY_ID] = entity_id
+
+ hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data)
+
+
+@bind_hass
+def set_temperature(hass, temperature=None, entity_id=None,
+ operation_mode=None):
+ """Set new target temperature."""
+ kwargs = {
+ key: value for key, value in [
+ (ATTR_TEMPERATURE, temperature),
+ (ATTR_ENTITY_ID, entity_id),
+ (ATTR_OPERATION_MODE, operation_mode)
+ ] if value is not None
+ }
+ _LOGGER.debug("set_temperature start data=%s", kwargs)
+ hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs)
+
+
+@bind_hass
+def set_operation_mode(hass, operation_mode, entity_id=None):
+ """Set new target operation mode."""
+ data = {ATTR_OPERATION_MODE: operation_mode}
+
+ if entity_id is not None:
+ data[ATTR_ENTITY_ID] = entity_id
+
+ hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data)
diff --git a/tests/components/water_heater/test_demo.py b/tests/components/water_heater/test_demo.py
new file mode 100644
index 00000000000..14fe57de99c
--- /dev/null
+++ b/tests/components/water_heater/test_demo.py
@@ -0,0 +1,118 @@
+"""The tests for the demo water_heater component."""
+import unittest
+
+from homeassistant.util.unit_system import (
+ IMPERIAL_SYSTEM
+)
+from homeassistant.setup import setup_component
+from homeassistant.components import water_heater
+
+from tests.common import get_test_home_assistant
+from tests.components.water_heater import common
+
+
+ENTITY_WATER_HEATER = 'water_heater.demo_water_heater'
+ENTITY_WATER_HEATER_CELSIUS = 'water_heater.demo_water_heater_celsius'
+
+
+class TestDemowater_heater(unittest.TestCase):
+ """Test the demo water_heater."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Set up things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.hass.config.units = IMPERIAL_SYSTEM
+ self.assertTrue(setup_component(self.hass, water_heater.DOMAIN, {
+ 'water_heater': {
+ 'platform': 'demo',
+ }}))
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """Stop down everything that was started."""
+ self.hass.stop()
+
+ def test_setup_params(self):
+ """Test the initial parameters."""
+ state = self.hass.states.get(ENTITY_WATER_HEATER)
+ self.assertEqual(119, state.attributes.get('temperature'))
+ self.assertEqual('off', state.attributes.get('away_mode'))
+ self.assertEqual("eco", state.attributes.get('operation_mode'))
+
+ def test_default_setup_params(self):
+ """Test the setup with default parameters."""
+ state = self.hass.states.get(ENTITY_WATER_HEATER)
+ self.assertEqual(110, state.attributes.get('min_temp'))
+ self.assertEqual(140, state.attributes.get('max_temp'))
+
+ def test_set_only_target_temp_bad_attr(self):
+ """Test setting the target temperature without required attribute."""
+ state = self.hass.states.get(ENTITY_WATER_HEATER)
+ self.assertEqual(119, state.attributes.get('temperature'))
+ common.set_temperature(self.hass, None, ENTITY_WATER_HEATER)
+ self.hass.block_till_done()
+ self.assertEqual(119, state.attributes.get('temperature'))
+
+ def test_set_only_target_temp(self):
+ """Test the setting of the target temperature."""
+ state = self.hass.states.get(ENTITY_WATER_HEATER)
+ self.assertEqual(119, state.attributes.get('temperature'))
+ common.set_temperature(self.hass, 110, ENTITY_WATER_HEATER)
+ self.hass.block_till_done()
+ state = self.hass.states.get(ENTITY_WATER_HEATER)
+ self.assertEqual(110, state.attributes.get('temperature'))
+
+ def test_set_operation_bad_attr_and_state(self):
+ """Test setting operation mode without required attribute.
+
+ Also check the state.
+ """
+ state = self.hass.states.get(ENTITY_WATER_HEATER)
+ self.assertEqual("eco", state.attributes.get('operation_mode'))
+ self.assertEqual("eco", state.state)
+ common.set_operation_mode(self.hass, None, ENTITY_WATER_HEATER)
+ self.hass.block_till_done()
+ state = self.hass.states.get(ENTITY_WATER_HEATER)
+ self.assertEqual("eco", state.attributes.get('operation_mode'))
+ self.assertEqual("eco", state.state)
+
+ def test_set_operation(self):
+ """Test setting of new operation mode."""
+ state = self.hass.states.get(ENTITY_WATER_HEATER)
+ self.assertEqual("eco", state.attributes.get('operation_mode'))
+ self.assertEqual("eco", state.state)
+ common.set_operation_mode(self.hass, "electric", ENTITY_WATER_HEATER)
+ self.hass.block_till_done()
+ state = self.hass.states.get(ENTITY_WATER_HEATER)
+ self.assertEqual("electric", state.attributes.get('operation_mode'))
+ self.assertEqual("electric", state.state)
+
+ def test_set_away_mode_bad_attr(self):
+ """Test setting the away mode without required attribute."""
+ state = self.hass.states.get(ENTITY_WATER_HEATER)
+ self.assertEqual('off', state.attributes.get('away_mode'))
+ common.set_away_mode(self.hass, None, ENTITY_WATER_HEATER)
+ self.hass.block_till_done()
+ self.assertEqual('off', state.attributes.get('away_mode'))
+
+ def test_set_away_mode_on(self):
+ """Test setting the away mode on/true."""
+ common.set_away_mode(self.hass, True, ENTITY_WATER_HEATER)
+ self.hass.block_till_done()
+ state = self.hass.states.get(ENTITY_WATER_HEATER)
+ self.assertEqual('on', state.attributes.get('away_mode'))
+
+ def test_set_away_mode_off(self):
+ """Test setting the away mode off/false."""
+ common.set_away_mode(self.hass, False, ENTITY_WATER_HEATER_CELSIUS)
+ self.hass.block_till_done()
+ state = self.hass.states.get(ENTITY_WATER_HEATER_CELSIUS)
+ self.assertEqual('off', state.attributes.get('away_mode'))
+
+ def test_set_only_target_temp_with_convert(self):
+ """Test the setting of the target temperature."""
+ state = self.hass.states.get(ENTITY_WATER_HEATER_CELSIUS)
+ self.assertEqual(113, state.attributes.get('temperature'))
+ common.set_temperature(self.hass, 114, ENTITY_WATER_HEATER_CELSIUS)
+ self.hass.block_till_done()
+ state = self.hass.states.get(ENTITY_WATER_HEATER_CELSIUS)
+ self.assertEqual(114, state.attributes.get('temperature'))
diff --git a/tests/components/weather/test_smhi.py b/tests/components/weather/test_smhi.py
new file mode 100644
index 00000000000..11a5028842b
--- /dev/null
+++ b/tests/components/weather/test_smhi.py
@@ -0,0 +1,292 @@
+"""Test for the smhi weather entity."""
+import asyncio
+import logging
+from datetime import datetime, timezone
+from unittest.mock import Mock, patch
+
+from homeassistant.components.weather import (
+ ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME,
+ ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE,
+ ATTR_FORECAST_TEMP_LOW, ATTR_WEATHER_VISIBILITY, ATTR_WEATHER_ATTRIBUTION,
+ ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED,
+ ATTR_FORECAST_PRECIPITATION, smhi as weather_smhi,
+ DOMAIN as WEATHER_DOMAIN)
+from homeassistant.const import TEMP_CELSIUS
+from homeassistant.core import HomeAssistant
+
+from tests.common import load_fixture, MockConfigEntry
+
+from homeassistant.components.smhi.const import ATTR_SMHI_CLOUDINESS
+
+_LOGGER = logging.getLogger(__name__)
+
+TEST_CONFIG = {
+ "name": "test",
+ "longitude": "17.84197",
+ "latitude": "59.32624"
+}
+
+
+async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None:
+ """Test for successfully setting up the smhi platform.
+
+ This test are deeper integrated with the core. Since only
+ config_flow is used the component are setup with
+ "async_forward_entry_setup". The actual result are tested
+ with the entity state rather than "per function" unity tests
+ """
+ from smhi.smhi_lib import APIURL_TEMPLATE
+
+ uri = APIURL_TEMPLATE.format(
+ TEST_CONFIG['longitude'], TEST_CONFIG['latitude'])
+ api_response = load_fixture('smhi.json')
+ aioclient_mock.get(uri, text=api_response)
+
+ entry = MockConfigEntry(domain='smhi', data=TEST_CONFIG)
+
+ await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN)
+ assert aioclient_mock.call_count == 1
+
+ # Testing the actual entity state for
+ # deeper testing than normal unity test
+ state = hass.states.get('weather.smhi_test')
+
+ assert state.state == 'sunny'
+ assert state.attributes[ATTR_SMHI_CLOUDINESS] == 50
+ assert state.attributes[ATTR_WEATHER_ATTRIBUTION].find('SMHI') >= 0
+ assert state.attributes[ATTR_WEATHER_HUMIDITY] == 55
+ assert state.attributes[ATTR_WEATHER_PRESSURE] == 1024
+ assert state.attributes[ATTR_WEATHER_TEMPERATURE] == 17
+ assert state.attributes[ATTR_WEATHER_VISIBILITY] == 50
+ assert state.attributes[ATTR_WEATHER_WIND_SPEED] == 7
+ assert state.attributes[ATTR_WEATHER_WIND_BEARING] == 134
+ _LOGGER.error(state.attributes)
+ assert len(state.attributes['forecast']) == 1
+
+ forecast = state.attributes['forecast'][0]
+ assert forecast[ATTR_FORECAST_TIME] == datetime(2018, 9, 2, 12, 0,
+ tzinfo=timezone.utc)
+ assert forecast[ATTR_FORECAST_TEMP] == 20
+ assert forecast[ATTR_FORECAST_TEMP_LOW] == 6
+ assert forecast[ATTR_FORECAST_PRECIPITATION] == 0
+ assert forecast[ATTR_FORECAST_CONDITION] == 'partlycloudy'
+
+
+async def test_setup_plattform(hass):
+ """Test that setup plattform does nothing."""
+ assert await weather_smhi.async_setup_platform(hass, None, None) is None
+
+
+def test_properties_no_data(hass: HomeAssistant) -> None:
+ """Test properties when no API data available."""
+ weather = weather_smhi.SmhiWeather('name', '10', '10')
+ weather.hass = hass
+
+ assert weather.name == 'name'
+ assert weather.should_poll is True
+ assert weather.temperature is None
+ assert weather.humidity is None
+ assert weather.wind_speed is None
+ assert weather.wind_bearing is None
+ assert weather.visibility is None
+ assert weather.pressure is None
+ assert weather.cloudiness is None
+ assert weather.condition is None
+ assert weather.forecast is None
+ assert weather.temperature_unit == TEMP_CELSIUS
+
+
+# pylint: disable=W0212
+def test_properties_unknown_symbol() -> None:
+ """Test behaviour when unknown symbol from API."""
+ hass = Mock()
+ data = Mock()
+ data.temperature = 5
+ data.mean_precipitation = 1
+ data.humidity = 5
+ data.wind_speed = 10
+ data.wind_direction = 180
+ data.horizontal_visibility = 6
+ data.pressure = 1008
+ data.cloudiness = 52
+ data.symbol = 100 # Faulty symbol
+ data.valid_time = datetime(2018, 1, 1, 0, 1, 2)
+
+ data2 = Mock()
+ data2.temperature = 5
+ data2.mean_precipitation = 1
+ data2.humidity = 5
+ data2.wind_speed = 10
+ data2.wind_direction = 180
+ data2.horizontal_visibility = 6
+ data2.pressure = 1008
+ data2.cloudiness = 52
+ data2.symbol = 100 # Faulty symbol
+ data2.valid_time = datetime(2018, 1, 1, 12, 1, 2)
+
+ data3 = Mock()
+ data3.temperature = 5
+ data3.mean_precipitation = 1
+ data3.humidity = 5
+ data3.wind_speed = 10
+ data3.wind_direction = 180
+ data3.horizontal_visibility = 6
+ data3.pressure = 1008
+ data3.cloudiness = 52
+ data3.symbol = 100 # Faulty symbol
+ data3.valid_time = datetime(2018, 1, 2, 12, 1, 2)
+
+ testdata = [
+ data,
+ data2,
+ data3
+ ]
+
+ weather = weather_smhi.SmhiWeather('name', '10', '10')
+ weather.hass = hass
+ weather._forecasts = testdata
+ assert weather.condition is None
+ forecast = weather.forecast[0]
+ assert forecast[ATTR_FORECAST_CONDITION] is None
+
+
+# pylint: disable=W0212
+async def test_refresh_weather_forecast_exceeds_retries(hass) -> None:
+ """Test the refresh weather forecast function."""
+ from smhi.smhi_lib import SmhiForecastException
+
+ with \
+ patch.object(hass.helpers.event, 'async_call_later') as call_later, \
+ patch.object(weather_smhi.SmhiWeather, 'get_weather_forecast',
+ side_effect=SmhiForecastException()):
+
+ weather = weather_smhi.SmhiWeather(
+ 'name', '17.0022', '62.0022')
+ weather.hass = hass
+ weather._fail_count = 2
+
+ await weather.async_update()
+ assert weather._forecasts is None
+ assert not call_later.mock_calls
+
+
+async def test_refresh_weather_forecast_timeout(hass) -> None:
+ """Test timeout exception."""
+ weather = weather_smhi.SmhiWeather(
+ 'name', '17.0022', '62.0022')
+ weather.hass = hass
+
+ with \
+ patch.object(hass.helpers.event, 'async_call_later') as call_later, \
+ patch.object(weather_smhi.SmhiWeather, 'retry_update'), \
+ patch.object(weather_smhi.SmhiWeather, 'get_weather_forecast',
+ side_effect=asyncio.TimeoutError):
+
+ await weather.async_update()
+ assert len(call_later.mock_calls) == 1
+ # Assert we are going to wait RETRY_TIMEOUT seconds
+ assert call_later.mock_calls[0][1][0] == weather_smhi.RETRY_TIMEOUT
+
+
+async def test_refresh_weather_forecast_exception() -> None:
+ """Test any exception."""
+ from smhi.smhi_lib import SmhiForecastException
+
+ hass = Mock()
+ weather = weather_smhi.SmhiWeather(
+ 'name', '17.0022', '62.0022')
+ weather.hass = hass
+
+ with \
+ patch.object(hass.helpers.event, 'async_call_later') as call_later, \
+ patch.object(weather_smhi, 'async_timeout'), \
+ patch.object(weather_smhi.SmhiWeather, 'retry_update'), \
+ patch.object(weather_smhi.SmhiWeather, 'get_weather_forecast',
+ side_effect=SmhiForecastException()):
+
+ hass.async_add_job = Mock()
+ call_later = hass.helpers.event.async_call_later
+
+ await weather.async_update()
+ assert len(call_later.mock_calls) == 1
+ # Assert we are going to wait RETRY_TIMEOUT seconds
+ assert call_later.mock_calls[0][1][0] == weather_smhi.RETRY_TIMEOUT
+
+
+async def test_retry_update():
+ """Test retry function of refresh forecast."""
+ hass = Mock()
+ weather = weather_smhi.SmhiWeather(
+ 'name', '17.0022', '62.0022')
+ weather.hass = hass
+
+ with patch.object(weather_smhi.SmhiWeather,
+ 'async_update') as update:
+ await weather.retry_update()
+ assert len(update.mock_calls) == 1
+
+
+def test_condition_class():
+ """Test condition class."""
+ def get_condition(index: int) -> str:
+ """Return condition given index."""
+ return [k for k, v in weather_smhi.CONDITION_CLASSES.items()
+ if index in v][0]
+
+ # SMHI definitions as follows, see
+ # http://opendata.smhi.se/apidocs/metfcst/parameters.html
+
+ # 1. Clear sky
+ assert get_condition(1) == 'sunny'
+ # 2. Nearly clear sky
+ assert get_condition(2) == 'sunny'
+ # 3. Variable cloudiness
+ assert get_condition(3) == 'partlycloudy'
+ # 4. Halfclear sky
+ assert get_condition(4) == 'partlycloudy'
+ # 5. Cloudy sky
+ assert get_condition(5) == 'cloudy'
+ # 6. Overcast
+ assert get_condition(6) == 'cloudy'
+ # 7. Fog
+ assert get_condition(7) == 'fog'
+ # 8. Light rain showers
+ assert get_condition(8) == 'rainy'
+ # 9. Moderate rain showers
+ assert get_condition(9) == 'rainy'
+ # 18. Light rain
+ assert get_condition(18) == 'rainy'
+ # 19. Moderate rain
+ assert get_condition(19) == 'rainy'
+ # 10. Heavy rain showers
+ assert get_condition(10) == 'pouring'
+ # 20. Heavy rain
+ assert get_condition(20) == 'pouring'
+ # 21. Thunder
+ assert get_condition(21) == 'lightning'
+ # 11. Thunderstorm
+ assert get_condition(11) == 'lightning-rainy'
+ # 15. Light snow showers
+ assert get_condition(15) == 'snowy'
+ # 16. Moderate snow showers
+ assert get_condition(16) == 'snowy'
+ # 17. Heavy snow showers
+ assert get_condition(17) == 'snowy'
+ # 25. Light snowfall
+ assert get_condition(25) == 'snowy'
+ # 26. Moderate snowfall
+ assert get_condition(26) == 'snowy'
+ # 27. Heavy snowfall
+ assert get_condition(27) == 'snowy'
+ # 12. Light sleet showers
+ assert get_condition(12) == 'snowy-rainy'
+ # 13. Moderate sleet showers
+ assert get_condition(13) == 'snowy-rainy'
+ # 14. Heavy sleet showers
+ assert get_condition(14) == 'snowy-rainy'
+ # 22. Light sleet
+ assert get_condition(22) == 'snowy-rainy'
+ # 23. Moderate sleet
+ assert get_condition(23) == 'snowy-rainy'
+ # 24. Heavy sleet
+ assert get_condition(24) == 'snowy-rainy'
diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py
index a2290d8aabf..b06dfb683b7 100644
--- a/tests/components/zwave/test_init.py
+++ b/tests/components/zwave/test_init.py
@@ -2,6 +2,7 @@
import asyncio
from collections import OrderedDict
from datetime import datetime
+from pytz import utc
import unittest
from unittest.mock import patch, MagicMock
@@ -22,13 +23,6 @@ from tests.common import (
from tests.mock.zwave import MockNetwork, MockNode, MockValue, MockEntityValues
-@asyncio.coroutine
-def test_missing_openzwave(hass):
- """Test that missing openzwave lib stops setup."""
- result = yield from async_setup_component(hass, 'zwave', {'zwave': {}})
- assert not result
-
-
@asyncio.coroutine
def test_valid_device_config(hass, mock_openzwave):
"""Test valid device config."""
@@ -41,6 +35,7 @@ def test_valid_device_config(hass, mock_openzwave):
'zwave': {
'device_config': device_config
}})
+ yield from hass.async_block_till_done()
assert result
@@ -57,6 +52,7 @@ def test_invalid_device_config(hass, mock_openzwave):
'zwave': {
'device_config': device_config
}})
+ yield from hass.async_block_till_done()
assert not result
@@ -81,6 +77,7 @@ def test_network_options(hass, mock_openzwave):
'usb_path': 'mock_usb_path',
'config_path': 'mock_config_path',
}})
+ yield from hass.async_block_till_done()
assert result
@@ -92,14 +89,16 @@ def test_network_options(hass, mock_openzwave):
@asyncio.coroutine
def test_auto_heal_midnight(hass, mock_openzwave):
"""Test network auto-heal at midnight."""
- assert (yield from async_setup_component(hass, 'zwave', {
+ yield from async_setup_component(hass, 'zwave', {
'zwave': {
'autoheal': True,
- }}))
+ }})
+ yield from hass.async_block_till_done()
+
network = hass.data[zwave.DATA_NETWORK]
assert not network.heal.called
- time = datetime(2017, 5, 6, 0, 0, 0)
+ time = utc.localize(datetime(2017, 5, 6, 0, 0, 0))
async_fire_time_changed(hass, time)
yield from hass.async_block_till_done()
assert network.heal.called
@@ -109,14 +108,16 @@ def test_auto_heal_midnight(hass, mock_openzwave):
@asyncio.coroutine
def test_auto_heal_disabled(hass, mock_openzwave):
"""Test network auto-heal disabled."""
- assert (yield from async_setup_component(hass, 'zwave', {
+ yield from async_setup_component(hass, 'zwave', {
'zwave': {
'autoheal': False,
- }}))
+ }})
+ yield from hass.async_block_till_done()
+
network = hass.data[zwave.DATA_NETWORK]
assert not network.heal.called
- time = datetime(2017, 5, 6, 0, 0, 0)
+ time = utc.localize(datetime(2017, 5, 6, 0, 0, 0))
async_fire_time_changed(hass, time)
yield from hass.async_block_till_done()
assert not network.heal.called
@@ -215,6 +216,7 @@ def test_node_discovery(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}})
+ yield from hass.async_block_till_done()
assert len(mock_receivers) == 1
@@ -235,6 +237,7 @@ async def test_unparsed_node_discovery(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect):
await async_setup_component(hass, 'zwave', {'zwave': {}})
+ await hass.async_block_till_done()
assert len(mock_receivers) == 1
@@ -282,6 +285,7 @@ def test_node_ignored(hass, mock_openzwave):
'zwave.mock_node': {
'ignored': True,
}}}})
+ yield from hass.async_block_till_done()
assert len(mock_receivers) == 1
@@ -303,6 +307,7 @@ def test_value_discovery(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}})
+ yield from hass.async_block_till_done()
assert len(mock_receivers) == 1
@@ -328,6 +333,7 @@ def test_value_discovery_existing_entity(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}})
+ yield from hass.async_block_till_done()
assert len(mock_receivers) == 1
@@ -373,6 +379,7 @@ def test_power_schemes(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}})
+ yield from hass.async_block_till_done()
assert len(mock_receivers) == 1
@@ -415,6 +422,7 @@ def test_network_ready(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}})
+ yield from hass.async_block_till_done()
assert len(mock_receivers) == 1
@@ -442,6 +450,7 @@ def test_network_complete(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}})
+ yield from hass.async_block_till_done()
assert len(mock_receivers) == 1
@@ -469,6 +478,7 @@ def test_network_complete_some_dead(hass, mock_openzwave):
with patch('pydispatch.dispatcher.connect', new=mock_connect):
yield from async_setup_component(hass, 'zwave', {'zwave': {}})
+ yield from hass.async_block_till_done()
assert len(mock_receivers) == 1
@@ -693,21 +703,24 @@ class TestZWaveDeviceEntityValues(unittest.TestCase):
const.COMMAND_CLASS_SWITCH_BINARY],
}}}
- values = zwave.ZWaveDeviceEntityValues(
- hass=self.hass,
- schema=self.mock_schema,
- primary_value=self.primary,
- zwave_config=self.zwave_config,
- device_config=self.device_config,
- registry=self.registry
- )
- values._check_entity_ready()
- self.hass.block_till_done()
+ with patch.object(zwave, 'async_dispatcher_send') as \
+ mock_dispatch_send:
- assert discovery.async_load_platform.called
- assert len(discovery.async_load_platform.mock_calls) == 1
- args = discovery.async_load_platform.mock_calls[0][1]
- assert args[1] == 'binary_sensor'
+ values = zwave.ZWaveDeviceEntityValues(
+ hass=self.hass,
+ schema=self.mock_schema,
+ primary_value=self.primary,
+ zwave_config=self.zwave_config,
+ device_config=self.device_config,
+ registry=self.registry
+ )
+ values._check_entity_ready()
+ self.hass.block_till_done()
+
+ assert mock_dispatch_send.called
+ assert len(mock_dispatch_send.mock_calls) == 1
+ args = mock_dispatch_send.mock_calls[0][1]
+ assert args[1] == 'zwave_new_binary_sensor'
@patch.object(zwave, 'get_platform')
@patch.object(zwave, 'discovery')
diff --git a/tests/fixtures/smhi.json b/tests/fixtures/smhi.json
new file mode 100644
index 00000000000..f66cc546018
--- /dev/null
+++ b/tests/fixtures/smhi.json
@@ -0,0 +1,1599 @@
+{
+ "approvedTime": "2018-09-01T14:06:18Z",
+ "referenceTime": "2018-09-01T14:00:00Z",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ [
+ 16.024394,
+ 63.341937
+ ]
+ ]
+ },
+ "timeSeries": [
+ {
+ "validTime": "2018-09-01T15:00:00Z",
+ "parameters": [
+ {
+ "name": "spp",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ -9
+ ]
+ },
+ {
+ "name": "pcat",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 1
+ ]
+ },
+ {
+ "name": "pmin",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 2
+ ]
+ },
+ {
+ "name": "pmax",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmedian",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 1
+ ]
+ },
+ {
+ "name": "tcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 4
+ ]
+ },
+ {
+ "name": "lcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "mcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "hcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "msl",
+ "levelType": "hmsl",
+ "level": 0,
+ "unit": "hPa",
+ "values": [
+ 1024.6
+ ]
+ },
+ {
+ "name": "t",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "Cel",
+ "values": [
+ 17
+ ]
+ },
+ {
+ "name": "vis",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "km",
+ "values": [
+ 50
+ ]
+ },
+ {
+ "name": "wd",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "degree",
+ "values": [
+ 134
+ ]
+ },
+ {
+ "name": "ws",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 1.9
+ ]
+ },
+ {
+ "name": "r",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "percent",
+ "values": [
+ 55
+ ]
+ },
+ {
+ "name": "tstm",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ 33
+ ]
+ },
+ {
+ "name": "gust",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 4.7
+ ]
+ },
+ {
+ "name": "Wsymb2",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 1
+ ]
+ }
+ ]
+ },
+ {
+ "validTime": "2018-09-02T00:00:00Z",
+ "parameters": [
+ {
+ "name": "msl",
+ "levelType": "hmsl",
+ "level": 0,
+ "unit": "hPa",
+ "values": [
+ 1026
+ ]
+ },
+ {
+ "name": "t",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "Cel",
+ "values": [
+ 6
+ ]
+ },
+ {
+ "name": "vis",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "km",
+ "values": [
+ 12
+ ]
+ },
+ {
+ "name": "wd",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "degree",
+ "values": [
+ 214
+ ]
+ },
+ {
+ "name": "ws",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 0.7
+ ]
+ },
+ {
+ "name": "r",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "percent",
+ "values": [
+ 87
+ ]
+ },
+ {
+ "name": "tstm",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "tcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "lcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "mcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "hcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "gust",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 1.5
+ ]
+ },
+ {
+ "name": "pmin",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmax",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "spp",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ -9
+ ]
+ },
+ {
+ "name": "pcat",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmedian",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "Wsymb2",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 1
+ ]
+ }
+ ]
+ },
+ {
+ "validTime": "2018-09-02T11:00:00Z",
+ "parameters": [
+ {
+ "name": "msl",
+ "levelType": "hmsl",
+ "level": 0,
+ "unit": "hPa",
+ "values": [
+ 1026.6
+ ]
+ },
+ {
+ "name": "t",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "Cel",
+ "values": [
+ 19.8
+ ]
+ },
+ {
+ "name": "vis",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "km",
+ "values": [
+ 50
+ ]
+ },
+ {
+ "name": "wd",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "degree",
+ "values": [
+ 201
+ ]
+ },
+ {
+ "name": "ws",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 1.8
+ ]
+ },
+ {
+ "name": "r",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "percent",
+ "values": [
+ 43
+ ]
+ },
+ {
+ "name": "tstm",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "tcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 6
+ ]
+ },
+ {
+ "name": "lcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "mcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "hcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 6
+ ]
+ },
+ {
+ "name": "gust",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 5.2
+ ]
+ },
+ {
+ "name": "pmin",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmax",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "spp",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ -9
+ ]
+ },
+ {
+ "name": "pcat",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmedian",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "Wsymb2",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 3
+ ]
+ }
+ ]
+ },
+ {
+ "validTime": "2018-09-02T12:00:00Z",
+ "parameters": [
+ {
+ "name": "msl",
+ "levelType": "hmsl",
+ "level": 0,
+ "unit": "hPa",
+ "values": [
+ 1026.5
+ ]
+ },
+ {
+ "name": "t",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "Cel",
+ "values": [
+ 20.6
+ ]
+ },
+ {
+ "name": "vis",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "km",
+ "values": [
+ 50
+ ]
+ },
+ {
+ "name": "wd",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "degree",
+ "values": [
+ 203
+ ]
+ },
+ {
+ "name": "ws",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 1.7
+ ]
+ },
+ {
+ "name": "r",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "percent",
+ "values": [
+ 43
+ ]
+ },
+ {
+ "name": "tstm",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "tcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 9
+ ]
+ },
+ {
+ "name": "lcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "mcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "hcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 6
+ ]
+ },
+ {
+ "name": "gust",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 5.1
+ ]
+ },
+ {
+ "name": "pmin",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmax",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "spp",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ -9
+ ]
+ },
+ {
+ "name": "pcat",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmedian",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "Wsymb2",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 3
+ ]
+ }
+ ]
+ },
+ {
+ "validTime": "2018-09-02T23:00:00Z",
+ "parameters": [
+ {
+ "name": "msl",
+ "levelType": "hmsl",
+ "level": 0,
+ "unit": "hPa",
+ "values": [
+ 1026
+ ]
+ },
+ {
+ "name": "t",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "Cel",
+ "values": [
+ 9.3
+ ]
+ },
+ {
+ "name": "vis",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "km",
+ "values": [
+ 19.4
+ ]
+ },
+ {
+ "name": "wd",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "degree",
+ "values": [
+ 95
+ ]
+ },
+ {
+ "name": "ws",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 0.5
+ ]
+ },
+ {
+ "name": "r",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "percent",
+ "values": [
+ 75
+ ]
+ },
+ {
+ "name": "tstm",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ 1
+ ]
+ },
+ {
+ "name": "tcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "lcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "mcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "hcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "gust",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 1.1
+ ]
+ },
+ {
+ "name": "pmin",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmax",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "spp",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ -9
+ ]
+ },
+ {
+ "name": "pcat",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmedian",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "Wsymb2",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 1
+ ]
+ }
+ ]
+ },
+ {
+ "validTime": "2018-09-03T00:00:00Z",
+ "parameters": [
+ {
+ "name": "msl",
+ "levelType": "hmsl",
+ "level": 0,
+ "unit": "hPa",
+ "values": [
+ 1025.9
+ ]
+ },
+ {
+ "name": "t",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "Cel",
+ "values": [
+ 8.5
+ ]
+ },
+ {
+ "name": "vis",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "km",
+ "values": [
+ 50
+ ]
+ },
+ {
+ "name": "wd",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "degree",
+ "values": [
+ 104
+ ]
+ },
+ {
+ "name": "ws",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 0.5
+ ]
+ },
+ {
+ "name": "r",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "percent",
+ "values": [
+ 73
+ ]
+ },
+ {
+ "name": "tstm",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ 1
+ ]
+ },
+ {
+ "name": "tcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "lcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "mcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "hcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "gust",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 1.1
+ ]
+ },
+ {
+ "name": "pmin",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmax",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "spp",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ -9
+ ]
+ },
+ {
+ "name": "pcat",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmedian",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "Wsymb2",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 1
+ ]
+ }
+ ]
+ },
+ {
+ "validTime": "2018-09-03T01:00:00Z",
+ "parameters": [
+ {
+ "name": "msl",
+ "levelType": "hmsl",
+ "level": 0,
+ "unit": "hPa",
+ "values": [
+ 1025.6
+ ]
+ },
+ {
+ "name": "t",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "Cel",
+ "values": [
+ 8
+ ]
+ },
+ {
+ "name": "vis",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "km",
+ "values": [
+ 50
+ ]
+ },
+ {
+ "name": "wd",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "degree",
+ "values": [
+ 116
+ ]
+ },
+ {
+ "name": "ws",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 0.3
+ ]
+ },
+ {
+ "name": "r",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "percent",
+ "values": [
+ 74
+ ]
+ },
+ {
+ "name": "tstm",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ 1
+ ]
+ },
+ {
+ "name": "tcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "lcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "mcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "hcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "gust",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 1
+ ]
+ },
+ {
+ "name": "pmin",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmax",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "spp",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ -9
+ ]
+ },
+ {
+ "name": "pcat",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmedian",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "Wsymb2",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 1
+ ]
+ }
+ ]
+ },
+ {
+ "validTime": "2018-09-04T12:00:00Z",
+ "parameters": [
+ {
+ "name": "msl",
+ "levelType": "hmsl",
+ "level": 0,
+ "unit": "hPa",
+ "values": [
+ 1020.5
+ ]
+ },
+ {
+ "name": "t",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "Cel",
+ "values": [
+ 19.2
+ ]
+ },
+ {
+ "name": "vis",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "km",
+ "values": [
+ 50
+ ]
+ },
+ {
+ "name": "wd",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "degree",
+ "values": [
+ 353
+ ]
+ },
+ {
+ "name": "ws",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 1.4
+ ]
+ },
+ {
+ "name": "r",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "percent",
+ "values": [
+ 60
+ ]
+ },
+ {
+ "name": "tstm",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "tcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 7
+ ]
+ },
+ {
+ "name": "lcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 3
+ ]
+ },
+ {
+ "name": "mcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 5
+ ]
+ },
+ {
+ "name": "hcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 4
+ ]
+ },
+ {
+ "name": "gust",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 4.7
+ ]
+ },
+ {
+ "name": "pmin",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmax",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "spp",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ -9
+ ]
+ },
+ {
+ "name": "pcat",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmedian",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "Wsymb2",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 4
+ ]
+ }
+ ]
+ },
+ {
+ "validTime": "2018-09-04T18:00:00Z",
+ "parameters": [
+ {
+ "name": "msl",
+ "levelType": "hmsl",
+ "level": 0,
+ "unit": "hPa",
+ "values": [
+ 1021.5
+ ]
+ },
+ {
+ "name": "t",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "Cel",
+ "values": [
+ 14.3
+ ]
+ },
+ {
+ "name": "vis",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "km",
+ "values": [
+ 50
+ ]
+ },
+ {
+ "name": "wd",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "degree",
+ "values": [
+ 333
+ ]
+ },
+ {
+ "name": "ws",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 2.3
+ ]
+ },
+ {
+ "name": "r",
+ "levelType": "hl",
+ "level": 2,
+ "unit": "percent",
+ "values": [
+ 81
+ ]
+ },
+ {
+ "name": "tstm",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "tcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 4
+ ]
+ },
+ {
+ "name": "lcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 1
+ ]
+ },
+ {
+ "name": "mcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 4
+ ]
+ },
+ {
+ "name": "hcc_mean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "octas",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "gust",
+ "levelType": "hl",
+ "level": 10,
+ "unit": "m/s",
+ "values": [
+ 4.5
+ ]
+ },
+ {
+ "name": "pmin",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmax",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0.2
+ ]
+ },
+ {
+ "name": "spp",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "percent",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pcat",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 4
+ ]
+ },
+ {
+ "name": "pmean",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "pmedian",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "kg/m2/h",
+ "values": [
+ 0
+ ]
+ },
+ {
+ "name": "Wsymb2",
+ "levelType": "hl",
+ "level": 0,
+ "unit": "category",
+ "values": [
+ 3
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py
index ab575c61789..cfd84dbc3b3 100644
--- a/tests/helpers/test_config_validation.py
+++ b/tests/helpers/test_config_validation.py
@@ -356,9 +356,15 @@ def test_string():
"""Test string validation."""
schema = vol.Schema(cv.string)
- with pytest.raises(vol.MultipleInvalid):
+ with pytest.raises(vol.Invalid):
schema(None)
+ with pytest.raises(vol.Invalid):
+ schema([])
+
+ with pytest.raises(vol.Invalid):
+ schema({})
+
for value in (True, 1, 'hello'):
schema(value)
diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py
index 1a0c248383b..c853d0b3447 100644
--- a/tests/helpers/test_entity_component.py
+++ b/tests/helpers/test_entity_component.py
@@ -415,3 +415,19 @@ async def test_unload_entry_fails_if_never_loaded(hass):
with pytest.raises(ValueError):
await component.async_unload_entry(entry)
+
+
+async def test_update_entity(hass):
+ """Test that we can update an entity with the helper."""
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ entity = MockEntity()
+ entity.async_update_ha_state = Mock(return_value=mock_coro())
+ await component.async_add_entities([entity])
+
+ # Called as part of async_add_entities
+ assert len(entity.async_update_ha_state.mock_calls) == 1
+
+ await hass.helpers.entity_component.async_update_entity(entity.entity_id)
+
+ assert len(entity.async_update_ha_state.mock_calls) == 2
+ assert entity.async_update_ha_state.mock_calls[-1][1][0] is True
diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py
index 5b57ca75d51..cb586698302 100644
--- a/tests/helpers/test_event.py
+++ b/tests/helpers/test_event.py
@@ -85,38 +85,6 @@ class TestEventHelpers(unittest.TestCase):
self.hass.block_till_done()
self.assertEqual(2, len(runs))
- def test_track_time_change(self):
- """Test tracking time change."""
- wildcard_runs = []
- specific_runs = []
-
- unsub = track_time_change(self.hass, lambda x: wildcard_runs.append(1))
- unsub_utc = track_utc_time_change(
- self.hass, lambda x: specific_runs.append(1), second=[0, 30])
-
- self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0))
- self.hass.block_till_done()
- self.assertEqual(1, len(specific_runs))
- self.assertEqual(1, len(wildcard_runs))
-
- self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15))
- self.hass.block_till_done()
- self.assertEqual(1, len(specific_runs))
- self.assertEqual(2, len(wildcard_runs))
-
- self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30))
- self.hass.block_till_done()
- self.assertEqual(2, len(specific_runs))
- self.assertEqual(3, len(wildcard_runs))
-
- unsub()
- unsub_utc()
-
- self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30))
- self.hass.block_till_done()
- self.assertEqual(2, len(specific_runs))
- self.assertEqual(3, len(wildcard_runs))
-
def test_track_state_change(self):
"""Test track_state_change."""
# 2 lists to track how often our callbacks get called
@@ -526,12 +494,64 @@ class TestEventHelpers(unittest.TestCase):
"""Send a time changed event."""
self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now})
+
+class TestTrackTimeChange(unittest.TestCase):
+ """Test track time change methods."""
+
+ def setUp(self):
+ """Set up the tests."""
+ self.orig_default_time_zone = dt_util.DEFAULT_TIME_ZONE
+ self.hass = get_test_home_assistant()
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ dt_util.set_default_time_zone(self.orig_default_time_zone)
+ self.hass.stop()
+
+ def _send_time_changed(self, now):
+ """Send a time changed event."""
+ self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now})
+
+ def test_track_time_change(self):
+ """Test tracking time change."""
+ wildcard_runs = []
+ specific_runs = []
+
+ unsub = track_time_change(self.hass,
+ lambda x: wildcard_runs.append(1))
+ unsub_utc = track_utc_time_change(
+ self.hass, lambda x: specific_runs.append(1), second=[0, 30])
+
+ self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0))
+ self.hass.block_till_done()
+ self.assertEqual(1, len(specific_runs))
+ self.assertEqual(1, len(wildcard_runs))
+
+ self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15))
+ self.hass.block_till_done()
+ self.assertEqual(1, len(specific_runs))
+ self.assertEqual(2, len(wildcard_runs))
+
+ self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30))
+ self.hass.block_till_done()
+ self.assertEqual(2, len(specific_runs))
+ self.assertEqual(3, len(wildcard_runs))
+
+ unsub()
+ unsub_utc()
+
+ self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30))
+ self.hass.block_till_done()
+ self.assertEqual(2, len(specific_runs))
+ self.assertEqual(3, len(wildcard_runs))
+
def test_periodic_task_minute(self):
"""Test periodic tasks per minute."""
specific_runs = []
unsub = track_utc_time_change(
- self.hass, lambda x: specific_runs.append(1), minute='/5')
+ self.hass, lambda x: specific_runs.append(1), minute='/5',
+ second=0)
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0))
self.hass.block_till_done()
@@ -556,7 +576,8 @@ class TestEventHelpers(unittest.TestCase):
specific_runs = []
unsub = track_utc_time_change(
- self.hass, lambda x: specific_runs.append(1), hour='/2')
+ self.hass, lambda x: specific_runs.append(1), hour='/2',
+ minute=0, second=0)
self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0))
self.hass.block_till_done()
@@ -566,7 +587,7 @@ class TestEventHelpers(unittest.TestCase):
self.hass.block_till_done()
self.assertEqual(1, len(specific_runs))
- self._send_time_changed(datetime(2014, 5, 24, 0, 0, 0))
+ self._send_time_changed(datetime(2014, 5, 25, 0, 0, 0))
self.hass.block_till_done()
self.assertEqual(2, len(specific_runs))
@@ -584,68 +605,138 @@ class TestEventHelpers(unittest.TestCase):
self.hass.block_till_done()
self.assertEqual(3, len(specific_runs))
- def test_periodic_task_day(self):
- """Test periodic tasks per day."""
- specific_runs = []
-
- unsub = track_utc_time_change(
- self.hass, lambda x: specific_runs.append(1), day='/2')
-
- self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0))
- self.hass.block_till_done()
- self.assertEqual(1, len(specific_runs))
-
- self._send_time_changed(datetime(2014, 5, 3, 12, 0, 0))
- self.hass.block_till_done()
- self.assertEqual(1, len(specific_runs))
-
- self._send_time_changed(datetime(2014, 5, 4, 0, 0, 0))
- self.hass.block_till_done()
- self.assertEqual(2, len(specific_runs))
-
- unsub()
-
- self._send_time_changed(datetime(2014, 5, 4, 0, 0, 0))
- self.hass.block_till_done()
- self.assertEqual(2, len(specific_runs))
-
- def test_periodic_task_year(self):
- """Test periodic tasks per year."""
- specific_runs = []
-
- unsub = track_utc_time_change(
- self.hass, lambda x: specific_runs.append(1), year='/2')
-
- self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0))
- self.hass.block_till_done()
- self.assertEqual(1, len(specific_runs))
-
- self._send_time_changed(datetime(2015, 5, 2, 0, 0, 0))
- self.hass.block_till_done()
- self.assertEqual(1, len(specific_runs))
-
- self._send_time_changed(datetime(2016, 5, 2, 0, 0, 0))
- self.hass.block_till_done()
- self.assertEqual(2, len(specific_runs))
-
- unsub()
-
- self._send_time_changed(datetime(2016, 5, 2, 0, 0, 0))
- self.hass.block_till_done()
- self.assertEqual(2, len(specific_runs))
-
def test_periodic_task_wrong_input(self):
"""Test periodic tasks with wrong input."""
specific_runs = []
with pytest.raises(ValueError):
track_utc_time_change(
- self.hass, lambda x: specific_runs.append(1), year='/two')
+ self.hass, lambda x: specific_runs.append(1), hour='/two')
self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0))
self.hass.block_till_done()
self.assertEqual(0, len(specific_runs))
+ def test_periodic_task_clock_rollback(self):
+ """Test periodic tasks with the time rolling backwards."""
+ specific_runs = []
+
+ unsub = track_utc_time_change(
+ self.hass, lambda x: specific_runs.append(1), hour='/2', minute=0,
+ second=0)
+
+ self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0))
+ self.hass.block_till_done()
+ self.assertEqual(1, len(specific_runs))
+
+ self._send_time_changed(datetime(2014, 5, 24, 23, 0, 0))
+ self.hass.block_till_done()
+ self.assertEqual(1, len(specific_runs))
+
+ self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0))
+ self.hass.block_till_done()
+ self.assertEqual(2, len(specific_runs))
+
+ self._send_time_changed(datetime(2014, 5, 24, 0, 0, 0))
+ self.hass.block_till_done()
+ self.assertEqual(3, len(specific_runs))
+
+ self._send_time_changed(datetime(2014, 5, 25, 2, 0, 0))
+ self.hass.block_till_done()
+ self.assertEqual(4, len(specific_runs))
+
+ unsub()
+
+ self._send_time_changed(datetime(2014, 5, 25, 2, 0, 0))
+ self.hass.block_till_done()
+ self.assertEqual(4, len(specific_runs))
+
+ def test_periodic_task_duplicate_time(self):
+ """Test periodic tasks not triggering on duplicate time."""
+ specific_runs = []
+
+ unsub = track_utc_time_change(
+ self.hass, lambda x: specific_runs.append(1), hour='/2', minute=0,
+ second=0)
+
+ self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0))
+ self.hass.block_till_done()
+ self.assertEqual(1, len(specific_runs))
+
+ self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0))
+ self.hass.block_till_done()
+ self.assertEqual(1, len(specific_runs))
+
+ self._send_time_changed(datetime(2014, 5, 25, 0, 0, 0))
+ self.hass.block_till_done()
+ self.assertEqual(2, len(specific_runs))
+
+ unsub()
+
+ def test_periodic_task_entering_dst(self):
+ """Test periodic task behavior when entering dst."""
+ tz = dt_util.get_time_zone('Europe/Vienna')
+ dt_util.set_default_time_zone(tz)
+ specific_runs = []
+
+ unsub = track_time_change(
+ self.hass, lambda x: specific_runs.append(1), hour=2, minute=30,
+ second=0)
+
+ self._send_time_changed(
+ tz.localize(datetime(2018, 3, 25, 1, 50, 0)))
+ self.hass.block_till_done()
+ self.assertEqual(0, len(specific_runs))
+
+ self._send_time_changed(
+ tz.localize(datetime(2018, 3, 25, 3, 50, 0)))
+ self.hass.block_till_done()
+ self.assertEqual(0, len(specific_runs))
+
+ self._send_time_changed(
+ tz.localize(datetime(2018, 3, 26, 1, 50, 0)))
+ self.hass.block_till_done()
+ self.assertEqual(0, len(specific_runs))
+
+ self._send_time_changed(
+ tz.localize(datetime(2018, 3, 26, 2, 50, 0)))
+ self.hass.block_till_done()
+ self.assertEqual(1, len(specific_runs))
+
+ unsub()
+
+ def test_periodic_task_leaving_dst(self):
+ """Test periodic task behavior when leaving dst."""
+ tz = dt_util.get_time_zone('Europe/Vienna')
+ dt_util.set_default_time_zone(tz)
+ specific_runs = []
+
+ unsub = track_time_change(
+ self.hass, lambda x: specific_runs.append(1), hour=2, minute=30,
+ second=0)
+
+ self._send_time_changed(
+ tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=False))
+ self.hass.block_till_done()
+ self.assertEqual(0, len(specific_runs))
+
+ self._send_time_changed(
+ tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=False))
+ self.hass.block_till_done()
+ self.assertEqual(1, len(specific_runs))
+
+ self._send_time_changed(
+ tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=True))
+ self.hass.block_till_done()
+ self.assertEqual(1, len(specific_runs))
+
+ self._send_time_changed(
+ tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True))
+ self.hass.block_till_done()
+ self.assertEqual(2, len(specific_runs))
+
+ unsub()
+
def test_call_later(self):
"""Test calling an action later."""
def action(): pass
diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py
index dc8106e0ed3..2ead38ba345 100644
--- a/tests/helpers/test_template.py
+++ b/tests/helpers/test_template.py
@@ -682,6 +682,32 @@ class TestHelpersTemplate(unittest.TestCase):
'None',
tpl.render())
+ def test_distance_function_with_2_entity_ids(self):
+ """Test distance function with 2 entity ids."""
+ self.hass.states.set('test.object', 'happy', {
+ 'latitude': 32.87336,
+ 'longitude': -117.22943,
+ })
+ self.hass.states.set('test.object_2', 'happy', {
+ 'latitude': self.hass.config.latitude,
+ 'longitude': self.hass.config.longitude,
+ })
+ tpl = template.Template(
+ '{{ distance("test.object", "test.object_2") | round }}',
+ self.hass)
+ self.assertEqual('187', tpl.render())
+
+ def test_distance_function_with_1_entity_1_coord(self):
+ """Test distance function with 1 entity_id and 1 coord."""
+ self.hass.states.set('test.object', 'happy', {
+ 'latitude': self.hass.config.latitude,
+ 'longitude': self.hass.config.longitude,
+ })
+ tpl = template.Template(
+ '{{ distance("test.object", "32.87336", "-117.22943") | round }}',
+ self.hass)
+ self.assertEqual('187', tpl.render())
+
def test_closest_function_home_vs_domain(self):
"""Test closest function home vs domain."""
self.hass.states.set('test_domain.object', 'happy', {
diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py
index 340118502b1..59777e2e6bb 100644
--- a/tests/test_config_entries.py
+++ b/tests/test_config_entries.py
@@ -11,7 +11,8 @@ from homeassistant.setup import async_setup_component
from homeassistant.util import dt
from tests.common import (
- MockModule, mock_coro, MockConfigEntry, async_fire_time_changed)
+ MockModule, mock_coro, MockConfigEntry, async_fire_time_changed,
+ MockPlatform, MockEntity)
@pytest.fixture
@@ -40,35 +41,87 @@ def test_call_setup_entry(hass):
assert len(mock_setup_entry.mock_calls) == 1
-@asyncio.coroutine
-def test_remove_entry(hass, manager):
+async def test_remove_entry(hass, manager):
"""Test that we can remove an entry."""
- mock_unload_entry = MagicMock(return_value=mock_coro(True))
+ async def mock_setup_entry(hass, entry):
+ """Mock setting up entry."""
+ hass.loop.create_task(hass.config_entries.async_forward_entry_setup(
+ entry, 'light'))
+ return True
+ async def mock_unload_entry(hass, entry):
+ """Mock unloading an entry."""
+ result = await hass.config_entries.async_forward_entry_unload(
+ entry, 'light')
+ assert result
+ return result
+
+ entity = MockEntity(
+ unique_id='1234',
+ name='Test Entity',
+ )
+
+ async def mock_setup_entry_platform(hass, entry, async_add_entities):
+ """Mock setting up platform."""
+ async_add_entities([entity])
+
+ loader.set_component(hass, 'test', MockModule(
+ 'test',
+ async_setup_entry=mock_setup_entry,
+ async_unload_entry=mock_unload_entry
+ ))
loader.set_component(
- hass, 'test',
- MockModule('comp', async_unload_entry=mock_unload_entry))
+ hass, 'light.test',
+ MockPlatform(async_setup_entry=mock_setup_entry_platform))
MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager)
- MockConfigEntry(
+ entry = MockConfigEntry(
domain='test',
entry_id='test2',
- state=config_entries.ENTRY_STATE_LOADED
- ).add_to_manager(manager)
+ )
+ entry.add_to_manager(manager)
MockConfigEntry(domain='test', entry_id='test3').add_to_manager(manager)
+ # Check all config entries exist
assert [item.entry_id for item in manager.async_entries()] == \
['test1', 'test2', 'test3']
- result = yield from manager.async_remove('test2')
+ # Setup entry
+ await entry.async_setup(hass)
+ await hass.async_block_till_done()
+ # Check entity state got added
+ assert hass.states.get('light.test_entity') is not None
+ # Group all_lights, light.test_entity
+ assert len(hass.states.async_all()) == 2
+
+ # Check entity got added to entity registry
+ ent_reg = await hass.helpers.entity_registry.async_get_registry()
+ assert len(ent_reg.entities) == 1
+ entity_entry = list(ent_reg.entities.values())[0]
+ assert entity_entry.config_entry_id == entry.entry_id
+
+ # Remove entry
+ result = await manager.async_remove('test2')
+ await hass.async_block_till_done()
+
+ # Check that unload went well and so no need to restart
assert result == {
'require_restart': False
}
+
+ # Check that config entry was removed.
assert [item.entry_id for item in manager.async_entries()] == \
['test1', 'test3']
- assert len(mock_unload_entry.mock_calls) == 1
+ # Check that entity state has been removed
+ assert hass.states.get('light.test_entity') is None
+ # Just Group all_lights
+ assert len(hass.states.async_all()) == 1
+
+ # Check that entity registry entry no longer references config_entry_id
+ entity_entry = list(ent_reg.entities.values())[0]
+ assert entity_entry.config_entry_id is None
@asyncio.coroutine
diff --git a/tests/test_core.py b/tests/test_core.py
index d88257abfb4..7ab624447c5 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -19,9 +19,9 @@ import homeassistant.util.dt as dt_util
from homeassistant.util.unit_system import (METRIC_SYSTEM)
from homeassistant.const import (
__version__, EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, CONF_UNIT_SYSTEM,
- ATTR_NOW, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP,
- EVENT_HOMEASSISTANT_CLOSE, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED,
- EVENT_SERVICE_EXECUTED)
+ ATTR_NOW, EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, ATTR_SECONDS,
+ EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_CLOSE,
+ EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_SERVICE_EXECUTED)
from tests.common import get_test_home_assistant, async_mock_service
@@ -916,12 +916,13 @@ def test_timer_out_of_sync(mock_monotonic, loop):
delay, callback, target = hass.loop.call_later.mock_calls[0][1]
- with patch.object(ha, '_LOGGER', MagicMock()) as mock_logger, \
- patch('homeassistant.core.dt_util.utcnow',
- return_value=datetime(2018, 12, 31, 3, 4, 8, 200000)):
+ with patch('homeassistant.core.dt_util.utcnow',
+ return_value=datetime(2018, 12, 31, 3, 4, 8, 200000)):
callback(target)
- assert len(mock_logger.error.mock_calls) == 1
+ event_type, event_data = hass.bus.async_fire.mock_calls[1][1]
+ assert event_type == EVENT_TIMER_OUT_OF_SYNC
+ assert abs(event_data[ATTR_SECONDS] - 2.433333) < 0.001
assert len(funcs) == 2
fire_time_event, stop_timer = funcs
diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py
index d670917c055..35a83de6bfb 100644
--- a/tests/util/test_dt.py
+++ b/tests/util/test_dt.py
@@ -164,3 +164,128 @@ class TestDateUtil(unittest.TestCase):
diff = dt_util.now() - timedelta(minutes=365*60*24)
self.assertEqual(dt_util.get_age(diff), "1 year")
+
+ def test_parse_time_expression(self):
+ """Test parse_time_expression."""
+ self.assertEqual(
+ [x for x in range(60)],
+ dt_util.parse_time_expression('*', 0, 59)
+ )
+ self.assertEqual(
+ [x for x in range(60)],
+ dt_util.parse_time_expression(None, 0, 59)
+ )
+
+ self.assertEqual(
+ [x for x in range(0, 60, 5)],
+ dt_util.parse_time_expression('/5', 0, 59)
+ )
+
+ self.assertEqual(
+ [1, 2, 3],
+ dt_util.parse_time_expression([2, 1, 3], 0, 59)
+ )
+
+ self.assertEqual(
+ [x for x in range(24)],
+ dt_util.parse_time_expression('*', 0, 23)
+ )
+
+ self.assertEqual(
+ [42],
+ dt_util.parse_time_expression(42, 0, 59)
+ )
+
+ self.assertRaises(ValueError, dt_util.parse_time_expression, 61, 0, 60)
+
+ def test_find_next_time_expression_time_basic(self):
+ """Test basic stuff for find_next_time_expression_time."""
+ def find(dt, hour, minute, second):
+ """Call test_find_next_time_expression_time."""
+ seconds = dt_util.parse_time_expression(second, 0, 59)
+ minutes = dt_util.parse_time_expression(minute, 0, 59)
+ hours = dt_util.parse_time_expression(hour, 0, 23)
+
+ return dt_util.find_next_time_expression_time(
+ dt, seconds, minutes, hours)
+
+ self.assertEqual(
+ datetime(2018, 10, 7, 10, 30, 0),
+ find(datetime(2018, 10, 7, 10, 20, 0), '*', '/30', 0)
+ )
+
+ self.assertEqual(
+ datetime(2018, 10, 7, 10, 30, 0),
+ find(datetime(2018, 10, 7, 10, 30, 0), '*', '/30', 0)
+ )
+
+ self.assertEqual(
+ datetime(2018, 10, 7, 12, 30, 30),
+ find(datetime(2018, 10, 7, 10, 30, 0), '/3', '/30', [30, 45])
+ )
+
+ self.assertEqual(
+ datetime(2018, 10, 8, 5, 0, 0),
+ find(datetime(2018, 10, 7, 10, 30, 0), 5, 0, 0)
+ )
+
+ def test_find_next_time_expression_time_dst(self):
+ """Test daylight saving time for find_next_time_expression_time."""
+ tz = dt_util.get_time_zone('Europe/Vienna')
+ dt_util.set_default_time_zone(tz)
+
+ def find(dt, hour, minute, second):
+ """Call test_find_next_time_expression_time."""
+ seconds = dt_util.parse_time_expression(second, 0, 59)
+ minutes = dt_util.parse_time_expression(minute, 0, 59)
+ hours = dt_util.parse_time_expression(hour, 0, 23)
+
+ return dt_util.find_next_time_expression_time(
+ dt, seconds, minutes, hours)
+
+ # Entering DST, clocks are rolled forward
+ self.assertEqual(
+ tz.localize(datetime(2018, 3, 26, 2, 30, 0)),
+ find(tz.localize(datetime(2018, 3, 25, 1, 50, 0)), 2, 30, 0)
+ )
+
+ self.assertEqual(
+ tz.localize(datetime(2018, 3, 26, 2, 30, 0)),
+ find(tz.localize(datetime(2018, 3, 25, 3, 50, 0)), 2, 30, 0)
+ )
+
+ self.assertEqual(
+ tz.localize(datetime(2018, 3, 26, 2, 30, 0)),
+ find(tz.localize(datetime(2018, 3, 26, 1, 50, 0)), 2, 30, 0)
+ )
+
+ # Leaving DST, clocks are rolled back
+ self.assertEqual(
+ tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=False),
+ find(tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=False),
+ 2, 30, 0)
+ )
+
+ self.assertEqual(
+ tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=False),
+ find(tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True),
+ 2, 30, 0)
+ )
+
+ self.assertEqual(
+ tz.localize(datetime(2018, 10, 28, 4, 30, 0), is_dst=False),
+ find(tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True),
+ 4, 30, 0)
+ )
+
+ self.assertEqual(
+ tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=True),
+ find(tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=True),
+ 2, 30, 0)
+ )
+
+ self.assertEqual(
+ tz.localize(datetime(2018, 10, 29, 2, 30, 0)),
+ find(tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=False),
+ 2, 30, 0)
+ )
diff --git a/tests/util/test_volume.py b/tests/util/test_volume.py
new file mode 100644
index 00000000000..e78e099d7d7
--- /dev/null
+++ b/tests/util/test_volume.py
@@ -0,0 +1,49 @@
+"""Test homeassistant volume utility functions."""
+
+import unittest
+import homeassistant.util.volume as volume_util
+from homeassistant.const import (VOLUME_LITERS, VOLUME_MILLILITERS,
+ VOLUME_GALLONS, VOLUME_FLUID_OUNCE)
+
+INVALID_SYMBOL = 'bob'
+VALID_SYMBOL = VOLUME_LITERS
+
+
+class TestVolumeUtil(unittest.TestCase):
+ """Test the volume utility functions."""
+
+ def test_convert_same_unit(self):
+ """Test conversion from any unit to same unit."""
+ self.assertEqual(2, volume_util.convert(2, VOLUME_LITERS,
+ VOLUME_LITERS))
+ self.assertEqual(3, volume_util.convert(3, VOLUME_MILLILITERS,
+ VOLUME_MILLILITERS))
+ self.assertEqual(4, volume_util.convert(4, VOLUME_GALLONS,
+ VOLUME_GALLONS))
+ self.assertEqual(5, volume_util.convert(5, VOLUME_FLUID_OUNCE,
+ VOLUME_FLUID_OUNCE))
+
+ def test_convert_invalid_unit(self):
+ """Test exception is thrown for invalid units."""
+ with self.assertRaises(ValueError):
+ volume_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL)
+
+ with self.assertRaises(ValueError):
+ volume_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL)
+
+ def test_convert_nonnumeric_value(self):
+ """Test exception is thrown for nonnumeric type."""
+ with self.assertRaises(TypeError):
+ volume_util.convert('a', VOLUME_GALLONS, VOLUME_LITERS)
+
+ def test_convert_from_liters(self):
+ """Test conversion from liters to other units."""
+ liters = 5
+ self.assertEqual(volume_util.convert(liters, VOLUME_LITERS,
+ VOLUME_GALLONS), 1.321)
+
+ def test_convert_from_gallons(self):
+ """Test conversion from gallons to other units."""
+ gallons = 5
+ self.assertEqual(volume_util.convert(gallons, VOLUME_GALLONS,
+ VOLUME_LITERS), 18.925)
diff --git a/virtualization/vagrant/Vagrantfile b/virtualization/vagrant/Vagrantfile
index e50c4e6de00..d3974d51a7a 100644
--- a/virtualization/vagrant/Vagrantfile
+++ b/virtualization/vagrant/Vagrantfile
@@ -13,4 +13,12 @@ Vagrant.configure(2) do |config|
vb.cpus = 2
vb.customize ['modifyvm', :id, '--memory', '1024']
end
+ config.vm.provider :hyperv do |h, override|
+ override.vm.box = "generic/debian9"
+ override.vm.hostname = "contrib-stretch"
+ h.vmname = "home-assistant"
+ h.cpus = 2
+ h.memory = 1024
+ h.maxmemory = 1024
+ end
end
diff --git a/virtualization/vagrant/provision.bat b/virtualization/vagrant/provision.bat
new file mode 100644
index 00000000000..c8174e939a1
--- /dev/null
+++ b/virtualization/vagrant/provision.bat
@@ -0,0 +1,50 @@
+@echo off
+call:main %*
+goto:eof
+
+:usage
+echo.############################################################
+echo.
+echo.Use `./provision.bat` to interact with HASS. E.g:
+echo.
+echo.- setup the environment: `./provision.bat start`
+echo.- restart HASS process: `./provision.bat restart`
+echo.- run test suit: `./provision.bat tests`
+echo.- destroy the host and start anew: `./provision.bat recreate`
+echo.
+echo.Official documentation at https://home-assistant.io/docs/installation/vagrant/
+echo.
+echo.############################################################'
+goto:eof
+
+:main
+if "%*"=="setup" (
+ if exist setup_done del setup_done
+ vagrant up --provision
+ copy /y nul setup_done
+) else (
+if "%*"=="tests" (
+ copy /y nul run_tests
+ vagrant provision
+) else (
+if "%*"=="restart" (
+ copy /y nul restart
+ vagrant provision
+) else (
+if "%*"=="start" (
+ vagrant up --provision
+) else (
+if "%*"=="stop" (
+ vagrant halt
+) else (
+if "%*"=="destroy" (
+ vagrant destroy -f
+) else (
+if "%*"=="recreate" (
+ if exist setup_done del setup_done
+ if exist restart del restart
+ vagrant destroy -f
+ vagrant up --provision
+) else (
+ call:usage
+)))))))
diff --git a/virtualization/vagrant/provision.sh b/virtualization/vagrant/provision.sh
index d4ef4e0b446..1d2eecddc73 100755
--- a/virtualization/vagrant/provision.sh
+++ b/virtualization/vagrant/provision.sh
@@ -76,6 +76,10 @@ run_tests() {
rsync -a --delete \
--exclude='*.tox' \
--exclude='*.git' \
+ --exclude='.vagrant' \
+ --exclude='lib64' \
+ --exclude='bin/python' \
+ --exclude='bin/python3' \
/home-assistant/ /home-assistant-tests/
cd /home-assistant-tests && tox || true
echo '############################################################'