Merge pull request #17809 from home-assistant/rc

0.81
This commit is contained in:
Paulus Schoutsen 2018-10-26 19:45:29 +02:00 committed by GitHub
commit e28170a0a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
472 changed files with 19297 additions and 2781 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -0,0 +1,252 @@
"""Permissions for Home Assistant."""
from typing import ( # noqa: F401
cast, Any, Callable, Dict, List, Mapping, Set, Tuple, Union)
import voluptuous as vol
from homeassistant.core import State
CategoryType = Union[Mapping[str, 'CategoryType'], bool, None]
PolicyType = Mapping[str, CategoryType]
# Default policy if group has no policy applied.
DEFAULT_POLICY = {
"entities": True
} # type: PolicyType
CAT_ENTITIES = 'entities'
ENTITY_DOMAINS = 'domains'
ENTITY_ENTITY_IDS = 'entity_ids'
VALUES_SCHEMA = vol.Any(True, vol.Schema({
str: True
}))
ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({
vol.Optional(ENTITY_DOMAINS): VALUES_SCHEMA,
vol.Optional(ENTITY_ENTITY_IDS): VALUES_SCHEMA,
}))
POLICY_SCHEMA = vol.Schema({
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
})
class AbstractPermissions:
"""Default permissions class."""
def check_entity(self, entity_id: str, *keys: str) -> bool:
"""Test if we can access entity."""
raise NotImplementedError
def filter_states(self, states: List[State]) -> List[State]:
"""Filter a list of states for what the user is allowed to see."""
raise NotImplementedError
class PolicyPermissions(AbstractPermissions):
"""Handle permissions."""
def __init__(self, policy: PolicyType) -> None:
"""Initialize the permission class."""
self._policy = policy
self._compiled = {} # type: Dict[str, Callable[..., bool]]
def check_entity(self, entity_id: str, *keys: str) -> bool:
"""Test if we can access entity."""
func = self._policy_func(CAT_ENTITIES, _compile_entities)
return func(entity_id, keys)
def filter_states(self, states: List[State]) -> List[State]:
"""Filter a list of states for what the user is allowed to see."""
func = self._policy_func(CAT_ENTITIES, _compile_entities)
keys = ('read',)
return [entity for entity in states if func(entity.entity_id, keys)]
def _policy_func(self, category: str,
compile_func: Callable[[CategoryType], Callable]) \
-> Callable[..., bool]:
"""Get a policy function."""
func = self._compiled.get(category)
if func:
return func
func = self._compiled[category] = compile_func(
self._policy.get(category))
return func
def __eq__(self, other: Any) -> bool:
"""Equals check."""
# pylint: disable=protected-access
return (isinstance(other, PolicyPermissions) and
other._policy == self._policy)
class _OwnerPermissions(AbstractPermissions):
"""Owner permissions."""
# pylint: disable=no-self-use
def check_entity(self, entity_id: str, *keys: str) -> bool:
"""Test if we can access entity."""
return True
def filter_states(self, states: List[State]) -> List[State]:
"""Filter a list of states for what the user is allowed to see."""
return states
OwnerPermissions = _OwnerPermissions() # pylint: disable=invalid-name
def _compile_entities(policy: CategoryType) \
-> Callable[[str, Tuple[str]], bool]:
"""Compile policy into a function that tests policy."""
# None, Empty Dict, False
if not policy:
def apply_policy_deny_all(entity_id: str, keys: Tuple[str]) -> bool:
"""Decline all."""
return False
return apply_policy_deny_all
if policy is True:
def apply_policy_allow_all(entity_id: str, keys: Tuple[str]) -> bool:
"""Approve all."""
return True
return apply_policy_allow_all
assert isinstance(policy, dict)
domains = policy.get(ENTITY_DOMAINS)
entity_ids = policy.get(ENTITY_ENTITY_IDS)
funcs = [] # type: List[Callable[[str, Tuple[str]], Union[None, bool]]]
# The order of these functions matter. The more precise are at the top.
# If a function returns None, they cannot handle it.
# If a function returns a boolean, that's the result to return.
# Setting entity_ids to a boolean is final decision for permissions
# So return right away.
if isinstance(entity_ids, bool):
def apply_entity_id_policy(entity_id: str, keys: Tuple[str]) -> bool:
"""Test if allowed entity_id."""
return entity_ids # type: ignore
return apply_entity_id_policy
if entity_ids is not None:
def allowed_entity_id(entity_id: str, keys: Tuple[str]) \
-> Union[None, bool]:
"""Test if allowed entity_id."""
return entity_ids.get(entity_id) # type: ignore
funcs.append(allowed_entity_id)
if isinstance(domains, bool):
def allowed_domain(entity_id: str, keys: Tuple[str]) \
-> Union[None, bool]:
"""Test if allowed domain."""
return domains
funcs.append(allowed_domain)
elif domains is not None:
def allowed_domain(entity_id: str, keys: Tuple[str]) \
-> Union[None, bool]:
"""Test if allowed domain."""
domain = entity_id.split(".", 1)[0]
return domains.get(domain) # type: ignore
funcs.append(allowed_domain)
# Can happen if no valid subcategories specified
if not funcs:
def apply_policy_deny_all_2(entity_id: str, keys: Tuple[str]) -> bool:
"""Decline all."""
return False
return apply_policy_deny_all_2
if len(funcs) == 1:
func = funcs[0]
def apply_policy_func(entity_id: str, keys: Tuple[str]) -> bool:
"""Apply a single policy function."""
return func(entity_id, keys) is True
return apply_policy_func
def apply_policy_funcs(entity_id: str, keys: Tuple[str]) -> bool:
"""Apply several policy functions."""
for func in funcs:
result = func(entity_id, keys)
if result is not None:
return result
return False
return apply_policy_funcs
def merge_policies(policies: List[PolicyType]) -> PolicyType:
"""Merge policies."""
new_policy = {} # type: Dict[str, CategoryType]
seen = set() # type: Set[str]
for policy in policies:
for category in policy:
if category in seen:
continue
seen.add(category)
new_policy[category] = _merge_policies([
policy.get(category) for policy in policies])
cast(PolicyType, new_policy)
return new_policy
def _merge_policies(sources: List[CategoryType]) -> CategoryType:
"""Merge a policy."""
# When merging policies, the most permissive wins.
# This means we order it like this:
# True > Dict > None
#
# True: allow everything
# Dict: specify more granular permissions
# None: no opinion
#
# If there are multiple sources with a dict as policy, we recursively
# merge each key in the source.
policy = None # type: CategoryType
seen = set() # type: Set[str]
for source in sources:
if source is None:
continue
# A source that's True will always win. Shortcut return.
if source is True:
return True
assert isinstance(source, dict)
if policy is None:
policy = {}
assert isinstance(policy, dict)
for key in source:
if key in seen:
continue
seen.add(key)
key_sources = []
for src in sources:
if isinstance(src, dict):
key_sources.append(src.get(key))
policy[key] = _merge_policies(key_sources)
return policy

View File

@ -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."""

View File

@ -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__)

View File

@ -0,0 +1,204 @@
"""
Each ElkM1 area will be created as a separate alarm_control_panel in HASS.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.elkm1/
"""
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import (
ATTR_CODE, ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMING, STATE_ALARM_DISARMED,
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED)
from homeassistant.components.elkm1 import (
DOMAIN as ELK_DOMAIN, create_elk_entities, ElkEntity)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
DEPENDENCIES = [ELK_DOMAIN]
SIGNAL_ARM_ENTITY = 'elkm1_arm'
SIGNAL_DISPLAY_MESSAGE = 'elkm1_display_message'
ELK_ALARM_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID, default=[]): cv.entity_ids,
vol.Required(ATTR_CODE): vol.All(vol.Coerce(int), vol.Range(0, 999999)),
})
DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids,
vol.Optional('clear', default=2): vol.In([0, 1, 2]),
vol.Optional('beep', default=False): cv.boolean,
vol.Optional('timeout', default=0): vol.Range(min=0, max=65535),
vol.Optional('line1', default=''): cv.string,
vol.Optional('line2', default=''): cv.string,
})
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the ElkM1 alarm platform."""
if discovery_info is None:
return
elk = hass.data[ELK_DOMAIN]['elk']
entities = create_elk_entities(hass, elk.areas, 'area', ElkArea, [])
async_add_entities(entities, True)
def _dispatch(signal, entity_ids, *args):
for entity_id in entity_ids:
async_dispatcher_send(
hass, '{}_{}'.format(signal, entity_id), *args)
def _arm_service(service):
entity_ids = service.data.get(ATTR_ENTITY_ID, [])
arm_level = _arm_services().get(service.service)
args = (arm_level, service.data.get(ATTR_CODE))
_dispatch(SIGNAL_ARM_ENTITY, entity_ids, *args)
for service in _arm_services():
hass.services.async_register(
alarm.DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA)
def _display_message_service(service):
entity_ids = service.data.get(ATTR_ENTITY_ID, [])
data = service.data
args = (data['clear'], data['beep'], data['timeout'],
data['line1'], data['line2'])
_dispatch(SIGNAL_DISPLAY_MESSAGE, entity_ids, *args)
hass.services.async_register(
alarm.DOMAIN, 'elkm1_alarm_display_message',
_display_message_service, DISPLAY_MESSAGE_SERVICE_SCHEMA)
def _arm_services():
from elkm1_lib.const import ArmLevel
return {
'elkm1_alarm_arm_vacation': ArmLevel.ARMED_VACATION.value,
'elkm1_alarm_arm_home_instant': ArmLevel.ARMED_STAY_INSTANT.value,
'elkm1_alarm_arm_night_instant': ArmLevel.ARMED_NIGHT_INSTANT.value,
}
class ElkArea(ElkEntity, alarm.AlarmControlPanel):
"""Representation of an Area / Partition within the ElkM1 alarm panel."""
def __init__(self, element, elk, elk_data):
"""Initialize Area as Alarm Control Panel."""
super().__init__(element, elk, elk_data)
self._changed_by_entity_id = ''
self._state = None
async def async_added_to_hass(self):
"""Register callback for ElkM1 changes."""
await super().async_added_to_hass()
for keypad in self._elk.keypads:
keypad.add_callback(self._watch_keypad)
async_dispatcher_connect(
self.hass, '{}_{}'.format(SIGNAL_ARM_ENTITY, self.entity_id),
self._arm_service)
async_dispatcher_connect(
self.hass, '{}_{}'.format(SIGNAL_DISPLAY_MESSAGE, self.entity_id),
self._display_message)
def _watch_keypad(self, keypad, changeset):
if keypad.area != self._element.index:
return
if changeset.get('last_user') is not None:
self._changed_by_entity_id = self.hass.data[
ELK_DOMAIN]['keypads'].get(keypad.index, '')
self.async_schedule_update_ha_state(True)
@property
def code_format(self):
"""Return the alarm code format."""
return '^[0-9]{4}([0-9]{2})?$'
@property
def state(self):
"""Return the state of the element."""
return self._state
@property
def device_state_attributes(self):
"""Attributes of the area."""
from elkm1_lib.const import AlarmState, ArmedStatus, ArmUpState
attrs = self.initial_attrs()
elmt = self._element
attrs['is_exit'] = elmt.is_exit
attrs['timer1'] = elmt.timer1
attrs['timer2'] = elmt.timer2
if elmt.armed_status is not None:
attrs['armed_status'] = \
ArmedStatus(elmt.armed_status).name.lower()
if elmt.arm_up_state is not None:
attrs['arm_up_state'] = ArmUpState(elmt.arm_up_state).name.lower()
if elmt.alarm_state is not None:
attrs['alarm_state'] = AlarmState(elmt.alarm_state).name.lower()
attrs['changed_by_entity_id'] = self._changed_by_entity_id
return attrs
def _element_changed(self, element, changeset):
from elkm1_lib.const import ArmedStatus
elk_state_to_hass_state = {
ArmedStatus.DISARMED.value: STATE_ALARM_DISARMED,
ArmedStatus.ARMED_AWAY.value: STATE_ALARM_ARMED_AWAY,
ArmedStatus.ARMED_STAY.value: STATE_ALARM_ARMED_HOME,
ArmedStatus.ARMED_STAY_INSTANT.value: STATE_ALARM_ARMED_HOME,
ArmedStatus.ARMED_TO_NIGHT.value: STATE_ALARM_ARMED_NIGHT,
ArmedStatus.ARMED_TO_NIGHT_INSTANT.value: STATE_ALARM_ARMED_NIGHT,
ArmedStatus.ARMED_TO_VACATION.value: STATE_ALARM_ARMED_AWAY,
}
if self._element.alarm_state is None:
self._state = None
elif self._area_is_in_alarm_state():
self._state = STATE_ALARM_TRIGGERED
elif self._entry_exit_timer_is_running():
self._state = STATE_ALARM_ARMING \
if self._element.is_exit else STATE_ALARM_PENDING
else:
self._state = elk_state_to_hass_state[self._element.armed_status]
def _entry_exit_timer_is_running(self):
return self._element.timer1 > 0 or self._element.timer2 > 0
def _area_is_in_alarm_state(self):
from elkm1_lib.const import AlarmState
return self._element.alarm_state >= AlarmState.FIRE_ALARM.value
async def async_alarm_disarm(self, code=None):
"""Send disarm command."""
self._element.disarm(int(code))
async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
from elkm1_lib.const import ArmLevel
self._element.arm(ArmLevel.ARMED_STAY.value, int(code))
async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
from elkm1_lib.const import ArmLevel
self._element.arm(ArmLevel.ARMED_AWAY.value, int(code))
async def async_alarm_arm_night(self, code=None):
"""Send arm night command."""
from elkm1_lib.const import ArmLevel
self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code))
async def _arm_service(self, arm_level, code):
self._element.arm(arm_level, code)
async def _display_message(self, clear, beep, timeout, line1, line2):
"""Display a message on all keypads for the area."""
self._element.display_message(clear, beep, timeout, line1, line2)

View File

@ -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.

View File

@ -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

View File

@ -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__)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,34 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Nu sunt disponibile servicii de notificare."
},
"error": {
"invalid_code": "Cod invalid, va rugam incercati din nou."
},
"step": {
"init": {
"description": "Selecta\u021bi unul dintre serviciile de notificare:",
"title": "Configura\u021bi o parol\u0103 unic\u0103 livrat\u0103 de o component\u0103 de notificare"
},
"setup": {
"description": "O parol\u0103 unic\u0103 a fost trimis\u0103 prin **notify.{notify_service}**. Introduce\u021bi parola mai jos:",
"title": "Verifica\u021bi configurarea"
}
},
"title": "Notifica\u021bi o parol\u0103 unic\u0103"
},
"totp": {
"error": {
"invalid_code": "Cod invalid, va rugam incercati din nou. Dac\u0103 primi\u021bi aceast\u0103 eroare \u00een mod consecvent, asigura\u021bi-v\u0103 c\u0103 ceasul sistemului dvs. Home Assistant este corect."
},
"step": {
"init": {
"title": "Configura\u021bi autentificarea cu doi factori utiliz\u00e2nd TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@ -0,0 +1,74 @@
"""
Offer geo location automation rules.
For more details about this automation trigger, please refer to the
documentation at
https://home-assistant.io/docs/automation/trigger/#geo-location-trigger
"""
import voluptuous as vol
from homeassistant.components.geo_location import DOMAIN
from homeassistant.core import callback
from homeassistant.const import (
CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE, EVENT_STATE_CHANGED)
from homeassistant.helpers import (
condition, config_validation as cv)
from homeassistant.helpers.config_validation import entity_domain
EVENT_ENTER = 'enter'
EVENT_LEAVE = 'leave'
DEFAULT_EVENT = EVENT_ENTER
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'geo_location',
vol.Required(CONF_SOURCE): cv.string,
vol.Required(CONF_ZONE): entity_domain('zone'),
vol.Required(CONF_EVENT, default=DEFAULT_EVENT):
vol.Any(EVENT_ENTER, EVENT_LEAVE),
})
def source_match(state, source):
"""Check if the state matches the provided source."""
return state and state.attributes.get('source') == source
async def async_trigger(hass, config, action):
"""Listen for state changes based on configuration."""
source = config.get(CONF_SOURCE).lower()
zone_entity_id = config.get(CONF_ZONE)
trigger_event = config.get(CONF_EVENT)
@callback
def state_change_listener(event):
"""Handle specific state changes."""
# Skip if the event is not a geo_location entity.
if not event.data.get('entity_id').startswith(DOMAIN):
return
# Skip if the event's source does not match the trigger's source.
from_state = event.data.get('old_state')
to_state = event.data.get('new_state')
if not source_match(from_state, source) \
and not source_match(to_state, source):
return
zone_state = hass.states.get(zone_entity_id)
from_match = condition.zone(hass, zone_state, from_state)
to_match = condition.zone(hass, zone_state, to_state)
# pylint: disable=too-many-boolean-expressions
if trigger_event == EVENT_ENTER and not from_match and to_match or \
trigger_event == EVENT_LEAVE and from_match and not to_match:
hass.async_run_job(action({
'trigger': {
'platform': 'geo_location',
'source': source,
'entity_id': event.data.get('entity_id'),
'from_state': from_state,
'to_state': to_state,
'zone': zone_state,
'event': trigger_event,
},
}, context=event.context))
return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener)

View File

@ -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',

View File

@ -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,
})

View File

@ -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)

View File

@ -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):

View File

@ -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):

View File

@ -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."""

View File

@ -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(

View File

@ -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)

View File

@ -0,0 +1,145 @@
"""
Support for OpenTherm Gateway binary sensors.
For more details about this platform, please refer to the documentation at
http://home-assistant.io/components/binary_sensor.opentherm_gw/
"""
import logging
from homeassistant.components.binary_sensor import (
BinarySensorDevice, ENTITY_ID_FORMAT)
from homeassistant.components.opentherm_gw import (
DATA_GW_VARS, DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import async_generate_entity_id
DEVICE_CLASS_COLD = 'cold'
DEVICE_CLASS_HEAT = 'heat'
DEVICE_CLASS_PROBLEM = 'problem'
DEPENDENCIES = ['opentherm_gw']
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the OpenTherm Gateway binary sensors."""
if discovery_info is None:
return
gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS]
sensor_info = {
# [device_class, friendly_name]
gw_vars.DATA_MASTER_CH_ENABLED: [
None, "Thermostat Central Heating Enabled"],
gw_vars.DATA_MASTER_DHW_ENABLED: [
None, "Thermostat Hot Water Enabled"],
gw_vars.DATA_MASTER_COOLING_ENABLED: [
None, "Thermostat Cooling Enabled"],
gw_vars.DATA_MASTER_OTC_ENABLED: [
None, "Thermostat Outside Temperature Correction Enabled"],
gw_vars.DATA_MASTER_CH2_ENABLED: [
None, "Thermostat Central Heating 2 Enabled"],
gw_vars.DATA_SLAVE_FAULT_IND: [
DEVICE_CLASS_PROBLEM, "Boiler Fault Indication"],
gw_vars.DATA_SLAVE_CH_ACTIVE: [
DEVICE_CLASS_HEAT, "Boiler Central Heating Status"],
gw_vars.DATA_SLAVE_DHW_ACTIVE: [
DEVICE_CLASS_HEAT, "Boiler Hot Water Status"],
gw_vars.DATA_SLAVE_FLAME_ON: [
DEVICE_CLASS_HEAT, "Boiler Flame Status"],
gw_vars.DATA_SLAVE_COOLING_ACTIVE: [
DEVICE_CLASS_COLD, "Boiler Cooling Status"],
gw_vars.DATA_SLAVE_CH2_ACTIVE: [
DEVICE_CLASS_HEAT, "Boiler Central Heating 2 Status"],
gw_vars.DATA_SLAVE_DIAG_IND: [
DEVICE_CLASS_PROBLEM, "Boiler Diagnostics Indication"],
gw_vars.DATA_SLAVE_DHW_PRESENT: [None, "Boiler Hot Water Present"],
gw_vars.DATA_SLAVE_CONTROL_TYPE: [None, "Boiler Control Type"],
gw_vars.DATA_SLAVE_COOLING_SUPPORTED: [None, "Boiler Cooling Support"],
gw_vars.DATA_SLAVE_DHW_CONFIG: [
None, "Boiler Hot Water Configuration"],
gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP: [
None, "Boiler Pump Commands Support"],
gw_vars.DATA_SLAVE_CH2_PRESENT: [
None, "Boiler Central Heating 2 Present"],
gw_vars.DATA_SLAVE_SERVICE_REQ: [
DEVICE_CLASS_PROBLEM, "Boiler Service Required"],
gw_vars.DATA_SLAVE_REMOTE_RESET: [None, "Boiler Remote Reset Support"],
gw_vars.DATA_SLAVE_LOW_WATER_PRESS: [
DEVICE_CLASS_PROBLEM, "Boiler Low Water Pressure"],
gw_vars.DATA_SLAVE_GAS_FAULT: [
DEVICE_CLASS_PROBLEM, "Boiler Gas Fault"],
gw_vars.DATA_SLAVE_AIR_PRESS_FAULT: [
DEVICE_CLASS_PROBLEM, "Boiler Air Pressure Fault"],
gw_vars.DATA_SLAVE_WATER_OVERTEMP: [
DEVICE_CLASS_PROBLEM, "Boiler Water Overtemperature"],
gw_vars.DATA_REMOTE_TRANSFER_DHW: [
None, "Remote Hot Water Setpoint Transfer Support"],
gw_vars.DATA_REMOTE_TRANSFER_MAX_CH: [
None, "Remote Maximum Central Heating Setpoint Write Support"],
gw_vars.DATA_REMOTE_RW_DHW: [
None, "Remote Hot Water Setpoint Write Support"],
gw_vars.DATA_REMOTE_RW_MAX_CH: [
None, "Remote Central Heating Setpoint Write Support"],
gw_vars.DATA_ROVRD_MAN_PRIO: [
None, "Remote Override Manual Change Priority"],
gw_vars.DATA_ROVRD_AUTO_PRIO: [
None, "Remote Override Program Change Priority"],
gw_vars.OTGW_GPIO_A_STATE: [None, "Gateway GPIO A State"],
gw_vars.OTGW_GPIO_B_STATE: [None, "Gateway GPIO B State"],
gw_vars.OTGW_IGNORE_TRANSITIONS: [None, "Gateway Ignore Transitions"],
gw_vars.OTGW_OVRD_HB: [None, "Gateway Override High Byte"],
}
sensors = []
for var in discovery_info:
device_class = sensor_info[var][0]
friendly_name = sensor_info[var][1]
entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, var, hass=hass)
sensors.append(OpenThermBinarySensor(entity_id, var, device_class,
friendly_name))
async_add_entities(sensors)
class OpenThermBinarySensor(BinarySensorDevice):
"""Represent an OpenTherm Gateway binary sensor."""
def __init__(self, entity_id, var, device_class, friendly_name):
"""Initialize the binary sensor."""
self.entity_id = entity_id
self._var = var
self._state = None
self._device_class = device_class
self._friendly_name = friendly_name
async def async_added_to_hass(self):
"""Subscribe to updates from the component."""
_LOGGER.debug(
"Added OpenTherm Gateway binary sensor %s", self._friendly_name)
async_dispatcher_connect(self.hass, SIGNAL_OPENTHERM_GW_UPDATE,
self.receive_report)
async def receive_report(self, status):
"""Handle status updates from the component."""
self._state = bool(status.get(self._var))
self.async_schedule_update_ha_state()
@property
def name(self):
"""Return the friendly name."""
return self._friendly_name
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self._state
@property
def device_class(self):
"""Return the class of this device."""
return self._device_class
@property
def should_poll(self):
"""Return False because entity pushes its state."""
return False

View File

@ -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."""

View File

@ -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)

View File

@ -0,0 +1,105 @@
"""
Support for Rflink binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.rflink/
"""
import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.components.rflink import (
CONF_ALIASES, CONF_DEVICES, RflinkDevice)
from homeassistant.const import (
CONF_FORCE_UPDATE, CONF_NAME, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.event as evt
CONF_OFF_DELAY = 'off_delay'
DEFAULT_FORCE_UPDATE = False
DEPENDENCIES = ['rflink']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICES, default={}): {
cv.string: vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE):
cv.boolean,
vol.Optional(CONF_OFF_DELAY): cv.positive_int,
vol.Optional(CONF_ALIASES, default=[]):
vol.All(cv.ensure_list, [cv.string]),
})
},
}, extra=vol.ALLOW_EXTRA)
def devices_from_config(domain_config):
"""Parse configuration and add Rflink sensor devices."""
devices = []
for device_id, config in domain_config[CONF_DEVICES].items():
device = RflinkBinarySensor(device_id, **config)
devices.append(device)
return devices
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Rflink platform."""
async_add_entities(devices_from_config(config))
class RflinkBinarySensor(RflinkDevice, BinarySensorDevice):
"""Representation of an Rflink binary sensor."""
def __init__(self, device_id, device_class=None,
force_update=None, off_delay=None,
**kwargs):
"""Handle sensor specific args and super init."""
self._state = None
self._device_class = device_class
self._force_update = force_update
self._off_delay = off_delay
self._delay_listener = None
super().__init__(device_id, **kwargs)
def _handle_event(self, event):
"""Domain specific event handler."""
command = event['command']
if command == 'on':
self._state = True
elif command == 'off':
self._state = False
if (self._state and self._off_delay is not None):
def off_delay_listener(now):
"""Switch device off after a delay."""
self._delay_listener = None
self._state = False
self.async_schedule_update_ha_state()
if self._delay_listener is not None:
self._delay_listener()
self._delay_listener = evt.async_call_later(
self.hass, self._off_delay, off_delay_listener)
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self._state
@property
def device_class(self):
"""Return the class of this sensor."""
return self._device_class
@property
def force_update(self):
"""Force update."""
return self._force_update

View File

@ -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."""

View File

@ -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__)

View File

@ -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']

View File

@ -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."""

View File

@ -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)

View File

@ -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)

View File

@ -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__)

View File

@ -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)

View File

@ -68,6 +68,7 @@ class GoogleCalendarData:
self.event = None
def _prepare_query(self):
# pylint: disable=import-error
from httplib2 import ServerNotFoundError
try:

View File

@ -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

View File

@ -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."""

View File

@ -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)])

View File

@ -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

View File

@ -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."""

View File

@ -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,

View File

@ -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,

View File

@ -7,9 +7,9 @@
"step": {
"confirm": {
"description": "Deseja configurar o Google Cast?",
"title": ""
"title": "Google Cast"
}
},
"title": ""
"title": "Google Cast"
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "Nu s-au g\u0103sit dispozitive Google Cast \u00een re\u021bea.",
"single_instance_allowed": "Este necesar\u0103 o singur\u0103 configura\u021bie a serviciului Google Cast."
},
"step": {
"confirm": {
"description": "Dori\u021bi s\u0103 configura\u021bi Google Cast?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,176 @@
"""
Support for Dyson Pure Hot+Cool link fan.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.dyson/
"""
import logging
from homeassistant.components.dyson import DYSON_DEVICES
from homeassistant.components.climate import (
ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE)
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
_LOGGER = logging.getLogger(__name__)
STATE_DIFFUSE = "Diffuse Mode"
STATE_FOCUS = "Focus Mode"
FAN_LIST = [STATE_FOCUS, STATE_DIFFUSE]
OPERATION_LIST = [STATE_HEAT, STATE_COOL]
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
| SUPPORT_OPERATION_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Dyson fan components."""
if discovery_info is None:
return
from libpurecoollink.dyson_pure_hotcool_link import DysonPureHotCoolLink
# Get Dyson Devices from parent component.
add_devices(
[DysonPureHotCoolLinkDevice(device)
for device in hass.data[DYSON_DEVICES]
if isinstance(device, DysonPureHotCoolLink)]
)
class DysonPureHotCoolLinkDevice(ClimateDevice):
"""Representation of a Dyson climate fan."""
def __init__(self, device):
"""Initialize the fan."""
self._device = device
self._current_temp = None
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
self.hass.async_add_job(self._device.add_message_listener,
self.on_message)
def on_message(self, message):
"""Call when new messages received from the climate."""
from libpurecoollink.dyson_pure_state import DysonPureHotCoolState
if isinstance(message, DysonPureHotCoolState):
_LOGGER.debug("Message received for climate device %s : %s",
self.name, message)
self.schedule_update_ha_state()
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def name(self):
"""Return the display name of this climate."""
return self._device.name
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
if self._device.environmental_state:
temperature_kelvin = self._device.environmental_state.temperature
if temperature_kelvin != 0:
self._current_temp = float("{0:.1f}".format(
temperature_kelvin - 273))
return self._current_temp
@property
def target_temperature(self):
"""Return the target temperature."""
heat_target = int(self._device.state.heat_target) / 10
return int(heat_target - 273)
@property
def current_humidity(self):
"""Return the current humidity."""
if self._device.environmental_state:
if self._device.environmental_state.humidity == 0:
return None
return self._device.environmental_state.humidity
return None
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
from libpurecoollink.const import HeatMode, HeatState
if self._device.state.heat_mode == HeatMode.HEAT_ON.value:
if self._device.state.heat_state == HeatState.HEAT_STATE_ON.value:
return STATE_HEAT
return STATE_IDLE
return STATE_COOL
@property
def operation_list(self):
"""Return the list of available operation modes."""
return OPERATION_LIST
@property
def current_fan_mode(self):
"""Return the fan setting."""
from libpurecoollink.const import FocusMode
if self._device.state.focus_mode == FocusMode.FOCUS_ON.value:
return STATE_FOCUS
return STATE_DIFFUSE
@property
def fan_list(self):
"""Return the list of available fan modes."""
return FAN_LIST
def set_temperature(self, **kwargs):
"""Set new target temperature."""
target_temp = kwargs.get(ATTR_TEMPERATURE)
if target_temp is None:
return
target_temp = int(target_temp)
_LOGGER.debug("Set %s temperature %s", self.name, target_temp)
# Limit the target temperature into acceptable range.
target_temp = min(self.max_temp, target_temp)
target_temp = max(self.min_temp, target_temp)
from libpurecoollink.const import HeatTarget, HeatMode
self._device.set_configuration(
heat_target=HeatTarget.celsius(target_temp),
heat_mode=HeatMode.HEAT_ON)
def set_fan_mode(self, fan_mode):
"""Set new fan mode."""
_LOGGER.debug("Set %s focus mode %s", self.name, fan_mode)
from libpurecoollink.const import FocusMode
if fan_mode == STATE_FOCUS:
self._device.set_configuration(focus_mode=FocusMode.FOCUS_ON)
elif fan_mode == STATE_DIFFUSE:
self._device.set_configuration(focus_mode=FocusMode.FOCUS_OFF)
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
_LOGGER.debug("Set %s heat mode %s", self.name, operation_mode)
from libpurecoollink.const import HeatMode
if operation_mode == STATE_HEAT:
self._device.set_configuration(heat_mode=HeatMode.HEAT_ON)
elif operation_mode == STATE_COOL:
self._device.set_configuration(heat_mode=HeatMode.HEAT_OFF)
@property
def min_temp(self):
"""Return the minimum temperature."""
return 1
@property
def max_temp(self):
"""Return the maximum temperature."""
return 37

View File

@ -0,0 +1,193 @@
"""
Support for control of Elk-M1 connected thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.elkm1/
"""
from homeassistant.components.climate import (
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PRECISION_WHOLE, STATE_AUTO,
STATE_COOL, STATE_FAN_ONLY, STATE_HEAT, STATE_IDLE, SUPPORT_AUX_HEAT,
SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE_HIGH,
SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice)
from homeassistant.components.elkm1 import (
DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities)
from homeassistant.const import STATE_ON
DEPENDENCIES = [ELK_DOMAIN]
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Create the Elk-M1 thermostat platform."""
if discovery_info is None:
return
elk = hass.data[ELK_DOMAIN]['elk']
async_add_entities(create_elk_entities(
hass, elk.thermostats, 'thermostat', ElkThermostat, []), True)
class ElkThermostat(ElkEntity, ClimateDevice):
"""Representation of an Elk-M1 Thermostat."""
def __init__(self, element, elk, elk_data):
"""Initialize climate entity."""
super().__init__(element, elk, elk_data)
self._state = None
@property
def supported_features(self):
"""Return the list of supported features."""
return (SUPPORT_OPERATION_MODE | SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT
| SUPPORT_TARGET_TEMPERATURE_HIGH
| SUPPORT_TARGET_TEMPERATURE_LOW)
@property
def temperature_unit(self):
"""Return the temperature unit."""
return self._temperature_unit
@property
def current_temperature(self):
"""Return the current temperature."""
return self._element.current_temp
@property
def target_temperature(self):
"""Return the temperature we are trying to reach."""
from elkm1_lib.const import ThermostatMode
if (self._element.mode == ThermostatMode.HEAT.value) or (
self._element.mode == ThermostatMode.EMERGENCY_HEAT.value):
return self._element.heat_setpoint
if self._element.mode == ThermostatMode.COOL.value:
return self._element.cool_setpoint
return None
@property
def target_temperature_high(self):
"""Return the high target temperature."""
return self._element.cool_setpoint
@property
def target_temperature_low(self):
"""Return the low target temperature."""
return self._element.heat_setpoint
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return 1
@property
def current_humidity(self):
"""Return the current humidity."""
return self._element.humidity
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return self._state
@property
def operation_list(self):
"""Return the list of available operation modes."""
return [STATE_IDLE, STATE_HEAT, STATE_COOL, STATE_AUTO, STATE_FAN_ONLY]
@property
def precision(self):
"""Return the precision of the system."""
return PRECISION_WHOLE
@property
def is_aux_heat_on(self):
"""Return if aux heater is on."""
from elkm1_lib.const import ThermostatMode
return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value
@property
def min_temp(self):
"""Return the minimum temperature supported."""
return 1
@property
def max_temp(self):
"""Return the maximum temperature supported."""
return 99
@property
def current_fan_mode(self):
"""Return the fan setting."""
from elkm1_lib.const import ThermostatFan
if self._element.fan == ThermostatFan.AUTO.value:
return STATE_AUTO
if self._element.fan == ThermostatFan.ON.value:
return STATE_ON
return None
def _elk_set(self, mode, fan):
from elkm1_lib.const import ThermostatSetting
if mode is not None:
self._element.set(ThermostatSetting.MODE.value, mode)
if fan is not None:
self._element.set(ThermostatSetting.FAN.value, fan)
async def async_set_operation_mode(self, operation_mode):
"""Set thermostat operation mode."""
from elkm1_lib.const import ThermostatFan, ThermostatMode
settings = {
STATE_IDLE: (ThermostatMode.OFF.value, ThermostatFan.AUTO.value),
STATE_HEAT: (ThermostatMode.HEAT.value, None),
STATE_COOL: (ThermostatMode.COOL.value, None),
STATE_AUTO: (ThermostatMode.AUTO.value, None),
STATE_FAN_ONLY: (ThermostatMode.OFF.value, ThermostatFan.ON.value)
}
self._elk_set(settings[operation_mode][0], settings[operation_mode][1])
async def async_turn_aux_heat_on(self):
"""Turn auxiliary heater on."""
from elkm1_lib.const import ThermostatMode
self._elk_set(ThermostatMode.EMERGENCY_HEAT.value, None)
async def async_turn_aux_heat_off(self):
"""Turn auxiliary heater off."""
from elkm1_lib.const import ThermostatMode
self._elk_set(ThermostatMode.HEAT.value, None)
@property
def fan_list(self):
"""Return the list of available fan modes."""
return [STATE_AUTO, STATE_ON]
async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
from elkm1_lib.const import ThermostatFan
if fan_mode == STATE_AUTO:
self._elk_set(None, ThermostatFan.AUTO.value)
elif fan_mode == STATE_ON:
self._elk_set(None, ThermostatFan.ON.value)
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
from elkm1_lib.const import ThermostatSetting
low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if low_temp is not None:
self._element.set(
ThermostatSetting.HEAT_SETPOINT.value, round(low_temp))
if high_temp is not None:
self._element.set(
ThermostatSetting.COOL_SETPOINT.value, round(high_temp))
def _element_changed(self, element, changeset):
from elkm1_lib.const import ThermostatFan, ThermostatMode
mode_to_state = {
ThermostatMode.OFF.value: STATE_IDLE,
ThermostatMode.COOL.value: STATE_COOL,
ThermostatMode.HEAT.value: STATE_HEAT,
ThermostatMode.EMERGENCY_HEAT.value: STATE_HEAT,
ThermostatMode.AUTO.value: STATE_AUTO,
}
self._state = mode_to_state.get(self._element.mode)
if self._state == STATE_IDLE and \
self._element.fan == ThermostatFan.ON.value:
self._state = STATE_FAN_ONLY

View File

@ -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:

View File

@ -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."""

View File

@ -0,0 +1,153 @@
"""
Support for mill wifi-enabled home heaters.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.mill/
"""
import logging
import voluptuous as vol
from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_FAN_MODE, SUPPORT_ON_OFF)
from homeassistant.const import (
ATTR_TEMPERATURE, CONF_PASSWORD, CONF_USERNAME,
STATE_ON, STATE_OFF, TEMP_CELSIUS)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
REQUIREMENTS = ['millheater==0.1.2']
_LOGGER = logging.getLogger(__name__)
MAX_TEMP = 35
MIN_TEMP = 5
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
SUPPORT_FAN_MODE | SUPPORT_ON_OFF)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
})
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Mill heater."""
from mill import Mill
mill_data_connection = Mill(config[CONF_USERNAME],
config[CONF_PASSWORD],
websession=async_get_clientsession(hass))
if not await mill_data_connection.connect():
_LOGGER.error("Failed to connect to Mill")
return
await mill_data_connection.update_heaters()
dev = []
for heater in mill_data_connection.heaters.values():
dev.append(MillHeater(heater, mill_data_connection))
async_add_entities(dev)
class MillHeater(ClimateDevice):
"""Representation of a Mill Thermostat device."""
def __init__(self, heater, mill_data_connection):
"""Initialize the thermostat."""
self._heater = heater
self._conn = mill_data_connection
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def available(self):
"""Return True if entity is available."""
return self._heater.device_status == 0 # weird api choice
@property
def unique_id(self):
"""Return a unique ID."""
return self._heater.device_id
@property
def name(self):
"""Return the name of the entity."""
return self._heater.name
@property
def temperature_unit(self):
"""Return the unit of measurement which this thermostat uses."""
return TEMP_CELSIUS
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._heater.set_temp
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return 1
@property
def current_temperature(self):
"""Return the current temperature."""
return self._heater.current_temp
@property
def current_fan_mode(self):
"""Return the fan setting."""
return STATE_ON if self._heater.fan_status == 1 else STATE_OFF
@property
def fan_list(self):
"""List of available fan modes."""
return [STATE_ON, STATE_OFF]
@property
def is_on(self):
"""Return true if heater is on."""
return self._heater.power_status == 1
@property
def min_temp(self):
"""Return the minimum temperature."""
return MIN_TEMP
@property
def max_temp(self):
"""Return the maximum temperature."""
return MAX_TEMP
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
await self._conn.set_heater_temp(self._heater.device_id,
int(temperature))
async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
fan_status = 1 if fan_mode == STATE_ON else 0
await self._conn.heater_control(self._heater.device_id,
fan_status=fan_status)
async def async_turn_on(self):
"""Turn Mill unit on."""
await self._conn.heater_control(self._heater.device_id,
power_status=1)
async def async_turn_off(self):
"""Turn Mill unit off."""
await self._conn.heater_control(self._heater.device_id,
power_status=0)
async def async_update(self):
"""Retrieve latest state."""
self._heater = await self._conn.update_device(self._heater.device_id)

View File

@ -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

View File

@ -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()

View File

@ -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:

View File

@ -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',

View File

@ -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()}

View File

@ -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()

View File

@ -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,
})

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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,

View File

@ -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)

View File

@ -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."""

View File

@ -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

View File

@ -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):

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -28,6 +28,6 @@
"title": "Op\u00e7\u00f5es extra de configura\u00e7\u00e3o para deCONZ"
}
},
"title": "deCONZ"
"title": "Gateway Zigbee deCONZ"
}
}

View File

@ -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"]

View File

@ -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)}

View File

@ -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:

View File

@ -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__)

View File

@ -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__)

View File

@ -0,0 +1,69 @@
"""
Support for Verizon FiOS Quantum Gateways.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.quantum_gateway/
"""
import logging
from requests.exceptions import RequestException
import voluptuous as vol
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA,
DeviceScanner)
from homeassistant.const import (CONF_HOST, CONF_PASSWORD)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['quantum-gateway==0.0.3']
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = 'myfiosgateway.com'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
def get_scanner(hass, config):
"""Validate the configuration and return a Quantum Gateway scanner."""
scanner = QuantumGatewayDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class QuantumGatewayDeviceScanner(DeviceScanner):
"""This class queries a Quantum Gateway."""
def __init__(self, config):
"""Initialize the scanner."""
from quantum_gateway import QuantumGatewayScanner
self.host = config[CONF_HOST]
self.password = config[CONF_PASSWORD]
_LOGGER.debug('Initializing')
try:
self.quantum = QuantumGatewayScanner(self.host, self.password)
self.success_init = self.quantum.success_init
except RequestException:
self.success_init = False
_LOGGER.error("Unable to connect to gateway. Check host.")
if not self.success_init:
_LOGGER.error("Unable to login to gateway. Check password and "
"host.")
def scan_devices(self):
"""Scan for new devices and return a list of found MACs."""
connected_devices = []
try:
connected_devices = self.quantum.scan_devices()
except RequestException:
_LOGGER.error("Unable to scan devices. Check connection to router")
return connected_devices
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
return self.quantum.get_device_name(device)

View File

@ -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

View File

@ -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

View File

@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover
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'),
}

View File

@ -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

View File

@ -0,0 +1,232 @@
"""
Support the ElkM1 Gold and ElkM1 EZ8 alarm / integration panels.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/elkm1/
"""
import logging
import re
import voluptuous as vol
from homeassistant.const import (
CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE, CONF_PASSWORD,
CONF_TEMPERATURE_UNIT, CONF_USERNAME, TEMP_FAHRENHEIT)
from homeassistant.core import HomeAssistant, callback # noqa
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType # noqa
DOMAIN = "elkm1"
REQUIREMENTS = ['elkm1-lib==0.7.10']
CONF_AREA = 'area'
CONF_COUNTER = 'counter'
CONF_KEYPAD = 'keypad'
CONF_OUTPUT = 'output'
CONF_SETTING = 'setting'
CONF_TASK = 'task'
CONF_THERMOSTAT = 'thermostat'
CONF_PLC = 'plc'
CONF_ZONE = 'zone'
CONF_ENABLED = 'enabled'
_LOGGER = logging.getLogger(__name__)
SUPPORTED_DOMAINS = ['alarm_control_panel', 'climate', 'light', 'scene',
'sensor', 'switch']
SPEAK_SERVICE_SCHEMA = vol.Schema({
vol.Required('number'):
vol.All(vol.Coerce(int), vol.Range(min=0, max=999))
})
def _host_validator(config):
"""Validate that a host is properly configured."""
if config[CONF_HOST].startswith('elks://'):
if CONF_USERNAME not in config or CONF_PASSWORD not in config:
raise vol.Invalid("Specify username and password for elks://")
elif not config[CONF_HOST].startswith('elk://') and not config[
CONF_HOST].startswith('serial://'):
raise vol.Invalid("Invalid host URL")
return config
def _elk_range_validator(rng):
def _housecode_to_int(val):
match = re.search(r'^([a-p])(0[1-9]|1[0-6]|[1-9])$', val.lower())
if match:
return (ord(match.group(1)) - ord('a')) * 16 + int(match.group(2))
raise vol.Invalid("Invalid range")
def _elk_value(val):
return int(val) if val.isdigit() else _housecode_to_int(val)
vals = [s.strip() for s in str(rng).split('-')]
start = _elk_value(vals[0])
end = start if len(vals) == 1 else _elk_value(vals[1])
return (start, end)
CONFIG_SCHEMA_SUBDOMAIN = vol.Schema({
vol.Optional(CONF_ENABLED, default=True): cv.boolean,
vol.Optional(CONF_INCLUDE, default=[]): [_elk_range_validator],
vol.Optional(CONF_EXCLUDE, default=[]): [_elk_range_validator],
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_USERNAME, default=''): cv.string,
vol.Optional(CONF_PASSWORD, default=''): cv.string,
vol.Optional(CONF_TEMPERATURE_UNIT, default=TEMP_FAHRENHEIT):
cv.temperature_unit,
vol.Optional(CONF_AREA): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_COUNTER): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_KEYPAD): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_OUTPUT): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_PLC): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_SETTING): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_TASK): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_THERMOSTAT): CONFIG_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_ZONE): CONFIG_SCHEMA_SUBDOMAIN,
},
_host_validator,
)
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
"""Set up the Elk M1 platform."""
from elkm1_lib.const import Max
import elkm1_lib as elkm1
configs = {
CONF_AREA: Max.AREAS.value,
CONF_COUNTER: Max.COUNTERS.value,
CONF_KEYPAD: Max.KEYPADS.value,
CONF_OUTPUT: Max.OUTPUTS.value,
CONF_PLC: Max.LIGHTS.value,
CONF_SETTING: Max.SETTINGS.value,
CONF_TASK: Max.TASKS.value,
CONF_THERMOSTAT: Max.THERMOSTATS.value,
CONF_ZONE: Max.ZONES.value,
}
def _included(ranges, set_to, values):
for rng in ranges:
if not rng[0] <= rng[1] <= len(values):
raise vol.Invalid("Invalid range {}".format(rng))
values[rng[0]-1:rng[1]] = [set_to] * (rng[1] - rng[0] + 1)
conf = hass_config[DOMAIN]
config = {'temperature_unit': conf[CONF_TEMPERATURE_UNIT]}
config['panel'] = {'enabled': True, 'included': [True]}
for item, max_ in configs.items():
config[item] = {'enabled': conf[item][CONF_ENABLED],
'included': [not conf[item]['include']] * max_}
try:
_included(conf[item]['include'], True, config[item]['included'])
_included(conf[item]['exclude'], False, config[item]['included'])
except (ValueError, vol.Invalid) as err:
_LOGGER.error("Config item: %s; %s", item, err)
return False
elk = elkm1.Elk({'url': conf[CONF_HOST], 'userid': conf[CONF_USERNAME],
'password': conf[CONF_PASSWORD]})
elk.connect()
_create_elk_services(hass, elk)
hass.data[DOMAIN] = {'elk': elk, 'config': config, 'keypads': {}}
for component in SUPPORTED_DOMAINS:
hass.async_create_task(
discovery.async_load_platform(hass, component, DOMAIN, {}))
return True
def _create_elk_services(hass, elk):
def _speak_word_service(service):
elk.panel.speak_word(service.data.get('number'))
def _speak_phrase_service(service):
elk.panel.speak_phrase(service.data.get('number'))
hass.services.async_register(
DOMAIN, 'speak_word', _speak_word_service, SPEAK_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, 'speak_phrase', _speak_phrase_service, SPEAK_SERVICE_SCHEMA)
def create_elk_entities(hass, elk_elements, element_type, class_, entities):
"""Create the ElkM1 devices of a particular class."""
elk_data = hass.data[DOMAIN]
if elk_data['config'][element_type]['enabled']:
elk = elk_data['elk']
for element in elk_elements:
if elk_data['config'][element_type]['included'][element.index]:
entities.append(class_(element, elk, elk_data))
return entities
class ElkEntity(Entity):
"""Base class for all Elk entities."""
def __init__(self, element, elk, elk_data):
"""Initialize the base of all Elk devices."""
self._elk = elk
self._element = element
self._temperature_unit = elk_data['config']['temperature_unit']
self._unique_id = 'elkm1_{}'.format(
self._element.default_name('_').lower())
@property
def name(self):
"""Name of the element."""
return self._element.name
@property
def unique_id(self):
"""Return unique id of the element."""
return self._unique_id
@property
def should_poll(self) -> bool:
"""Don't poll this device."""
return False
@property
def device_state_attributes(self):
"""Return the default attributes of the element."""
return {**self._element.as_dict(), **self.initial_attrs()}
@property
def available(self):
"""Is the entity available to be updated."""
return self._elk.is_connected()
def initial_attrs(self):
"""Return the underlying element's attributes as a dict."""
attrs = {}
attrs['index'] = self._element.index + 1
return attrs
def _element_changed(self, element, changeset):
pass
@callback
def _element_callback(self, element, changeset):
"""Handle callback from an Elk element that has changed."""
self._element_changed(element, changeset)
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Register callback for ElkM1 changes and update entity state."""
self._element.add_callback(self._element_callback)
self._element_callback(self._element, {})

View File

@ -0,0 +1,12 @@
speak_word:
description: Speak a word. See list of words in ElkM1 ASCII Protocol documentation.
fields:
number:
description: Word number to speak.
example: 142
speak_phrase:
description: Speak a phrase. See list of phrases in ElkM1 ASCII Protocol documentation.
fields:
number:
description: Phrase number to speak.
example: 42

View File

@ -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

View File

@ -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.',

View File

@ -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

View File

@ -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)

View File

@ -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',

View File

@ -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

View File

@ -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."""

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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")

View File

@ -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)

View File

@ -40,6 +40,9 @@ set:
add_entities:
description: List of members they will change on group listening.
example: domain.entity_id1, domain.entity_id2
all:
description: Enable this option if the group should only turn on when all entities are on.
example: True
remove:
description: Remove a user group.

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