Merge pull request #18776 from home-assistant/rc

0.83
This commit is contained in:
Paulus Schoutsen 2018-11-29 11:45:20 +01:00 committed by GitHub
commit c6c55c4419
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
444 changed files with 16617 additions and 7403 deletions

View File

@ -120,6 +120,9 @@ omit =
homeassistant/components/eufy.py homeassistant/components/eufy.py
homeassistant/components/*/eufy.py homeassistant/components/*/eufy.py
homeassistant/components/fibaro.py
homeassistant/components/*/fibaro.py
homeassistant/components/gc100.py homeassistant/components/gc100.py
homeassistant/components/*/gc100.py homeassistant/components/*/gc100.py
@ -203,6 +206,9 @@ omit =
homeassistant/components/logi_circle.py homeassistant/components/logi_circle.py
homeassistant/components/*/logi_circle.py homeassistant/components/*/logi_circle.py
homeassistant/components/lupusec.py
homeassistant/components/*/lupusec.py
homeassistant/components/lutron.py homeassistant/components/lutron.py
homeassistant/components/*/lutron.py homeassistant/components/*/lutron.py
@ -256,6 +262,10 @@ omit =
homeassistant/components/pilight.py homeassistant/components/pilight.py
homeassistant/components/*/pilight.py homeassistant/components/*/pilight.py
homeassistant/components/point/__init__.py
homeassistant/components/point/const.py
homeassistant/components/*/point.py
homeassistant/components/switch/qwikswitch.py homeassistant/components/switch/qwikswitch.py
homeassistant/components/light/qwikswitch.py homeassistant/components/light/qwikswitch.py
@ -265,7 +275,7 @@ omit =
homeassistant/components/raincloud.py homeassistant/components/raincloud.py
homeassistant/components/*/raincloud.py homeassistant/components/*/raincloud.py
homeassistant/components/rainmachine/* homeassistant/components/rainmachine/__init__.py
homeassistant/components/*/rainmachine.py homeassistant/components/*/rainmachine.py
homeassistant/components/raspihats.py homeassistant/components/raspihats.py
@ -333,6 +343,9 @@ omit =
homeassistant/components/toon.py homeassistant/components/toon.py
homeassistant/components/*/toon.py homeassistant/components/*/toon.py
homeassistant/components/tplink_lte.py
homeassistant/components/*/tplink_lte.py
homeassistant/components/tradfri.py homeassistant/components/tradfri.py
homeassistant/components/*/tradfri.py homeassistant/components/*/tradfri.py
@ -365,6 +378,9 @@ omit =
homeassistant/components/*/webostv.py homeassistant/components/*/webostv.py
homeassistant/components/w800rf32.py
homeassistant/components/*/w800rf32.py
homeassistant/components/wemo.py homeassistant/components/wemo.py
homeassistant/components/*/wemo.py homeassistant/components/*/wemo.py
@ -474,6 +490,7 @@ omit =
homeassistant/components/device_tracker/freebox.py homeassistant/components/device_tracker/freebox.py
homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/google_maps.py homeassistant/components/device_tracker/google_maps.py
homeassistant/components/device_tracker/googlehome.py
homeassistant/components/device_tracker/gpslogger.py homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/hitron_coda.py homeassistant/components/device_tracker/hitron_coda.py
homeassistant/components/device_tracker/huawei_router.py homeassistant/components/device_tracker/huawei_router.py
@ -496,6 +513,7 @@ omit =
homeassistant/components/device_tracker/tile.py homeassistant/components/device_tracker/tile.py
homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/tplink.py homeassistant/components/device_tracker/tplink.py
homeassistant/components/device_tracker/traccar.py
homeassistant/components/device_tracker/trackr.py homeassistant/components/device_tracker/trackr.py
homeassistant/components/device_tracker/ubus.py homeassistant/components/device_tracker/ubus.py
homeassistant/components/downloader.py homeassistant/components/downloader.py
@ -530,6 +548,7 @@ omit =
homeassistant/components/light/lw12wifi.py homeassistant/components/light/lw12wifi.py
homeassistant/components/light/mystrom.py homeassistant/components/light/mystrom.py
homeassistant/components/light/nanoleaf_aurora.py homeassistant/components/light/nanoleaf_aurora.py
homeassistant/components/light/niko_home_control.py
homeassistant/components/light/opple.py homeassistant/components/light/opple.py
homeassistant/components/light/osramlightify.py homeassistant/components/light/osramlightify.py
homeassistant/components/light/piglow.py homeassistant/components/light/piglow.py
@ -581,6 +600,7 @@ omit =
homeassistant/components/media_player/nadtcp.py homeassistant/components/media_player/nadtcp.py
homeassistant/components/media_player/onkyo.py homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/openhome.py homeassistant/components/media_player/openhome.py
homeassistant/components/media_player/panasonic_bluray.py
homeassistant/components/media_player/panasonic_viera.py homeassistant/components/media_player/panasonic_viera.py
homeassistant/components/media_player/pandora.py homeassistant/components/media_player/pandora.py
homeassistant/components/media_player/philips_js.py homeassistant/components/media_player/philips_js.py
@ -696,6 +716,7 @@ omit =
homeassistant/components/sensor/fints.py homeassistant/components/sensor/fints.py
homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/flunearyou.py
homeassistant/components/sensor/folder.py homeassistant/components/sensor/folder.py
homeassistant/components/sensor/foobot.py homeassistant/components/sensor/foobot.py
homeassistant/components/sensor/fritzbox_callmonitor.py homeassistant/components/sensor/fritzbox_callmonitor.py
@ -720,6 +741,7 @@ omit =
homeassistant/components/sensor/kwb.py homeassistant/components/sensor/kwb.py
homeassistant/components/sensor/lacrosse.py homeassistant/components/sensor/lacrosse.py
homeassistant/components/sensor/lastfm.py homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/launch_library.py
homeassistant/components/sensor/linky.py homeassistant/components/sensor/linky.py
homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/linux_battery.py
homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/loopenergy.py
@ -763,10 +785,12 @@ omit =
homeassistant/components/sensor/rainbird.py homeassistant/components/sensor/rainbird.py
homeassistant/components/sensor/ripple.py homeassistant/components/sensor/ripple.py
homeassistant/components/sensor/rtorrent.py homeassistant/components/sensor/rtorrent.py
homeassistant/components/sensor/ruter.py
homeassistant/components/sensor/scrape.py homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/sensehat.py homeassistant/components/sensor/sensehat.py
homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/serial.py homeassistant/components/sensor/serial.py
homeassistant/components/sensor/seventeentrack.py
homeassistant/components/sensor/sht31.py homeassistant/components/sensor/sht31.py
homeassistant/components/sensor/shodan.py homeassistant/components/sensor/shodan.py
homeassistant/components/sensor/sigfox.py homeassistant/components/sensor/sigfox.py
@ -786,9 +810,11 @@ omit =
homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/syncthru.py homeassistant/components/sensor/syncthru.py
homeassistant/components/sensor/synologydsm.py homeassistant/components/sensor/synologydsm.py
homeassistant/components/sensor/srp_energy.py
homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/sytadin.py homeassistant/components/sensor/sytadin.py
homeassistant/components/sensor/tank_utility.py homeassistant/components/sensor/tank_utility.py
homeassistant/components/sensor/tautulli.py
homeassistant/components/sensor/ted5000.py homeassistant/components/sensor/ted5000.py
homeassistant/components/sensor/temper.py homeassistant/components/sensor/temper.py
homeassistant/components/sensor/thermoworks_smoke.py homeassistant/components/sensor/thermoworks_smoke.py

View File

@ -13,6 +13,7 @@
## Checklist: ## Checklist:
- [ ] The code change is tested and works locally. - [ ] The code change is tested and works locally.
- [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** - [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
- [ ] There is no commented out code in this PR.
If user exposed functionality or configuration variables are added/changed: If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) - [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)

View File

@ -102,6 +102,7 @@ homeassistant/components/sensor/darksky.py @fabaff
homeassistant/components/sensor/file.py @fabaff homeassistant/components/sensor/file.py @fabaff
homeassistant/components/sensor/filter.py @dgomes homeassistant/components/sensor/filter.py @dgomes
homeassistant/components/sensor/fixer.py @fabaff homeassistant/components/sensor/fixer.py @fabaff
homeassistant/components/sensor/flunearyou.py.py @bachya
homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/gitter.py @fabaff homeassistant/components/sensor/gitter.py @fabaff
homeassistant/components/sensor/glances.py @fabaff homeassistant/components/sensor/glances.py @fabaff
@ -109,7 +110,6 @@ homeassistant/components/sensor/gpsd.py @fabaff
homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/jewish_calendar.py @tsvi homeassistant/components/sensor/jewish_calendar.py @tsvi
homeassistant/components/sensor/linux_battery.py @fabaff homeassistant/components/sensor/linux_battery.py @fabaff
homeassistant/components/sensor/luftdaten.py @fabaff
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/min_max.py @fabaff homeassistant/components/sensor/min_max.py @fabaff
homeassistant/components/sensor/moon.py @fabaff homeassistant/components/sensor/moon.py @fabaff
@ -121,6 +121,7 @@ homeassistant/components/sensor/pvoutput.py @fabaff
homeassistant/components/sensor/qnap.py @colinodell homeassistant/components/sensor/qnap.py @colinodell
homeassistant/components/sensor/scrape.py @fabaff homeassistant/components/sensor/scrape.py @fabaff
homeassistant/components/sensor/serial.py @fabaff homeassistant/components/sensor/serial.py @fabaff
homeassistant/components/sensor/seventeentrack.py @bachya
homeassistant/components/sensor/shodan.py @fabaff homeassistant/components/sensor/shodan.py @fabaff
homeassistant/components/sensor/sma.py @kellerza homeassistant/components/sensor/sma.py @kellerza
homeassistant/components/sensor/sql.py @dgomes homeassistant/components/sensor/sql.py @dgomes
@ -189,6 +190,8 @@ homeassistant/components/*/konnected.py @heythisisnate
# L # L
homeassistant/components/lifx.py @amelchio homeassistant/components/lifx.py @amelchio
homeassistant/components/*/lifx.py @amelchio homeassistant/components/*/lifx.py @amelchio
homeassistant/components/luftdaten/* @fabaff
homeassistant/components/*/luftdaten.py @fabaff
# M # M
homeassistant/components/matrix.py @tinloaf homeassistant/components/matrix.py @tinloaf

View File

@ -13,6 +13,7 @@ from homeassistant.core import callback, HomeAssistant
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import auth_store, models from . import auth_store, models
from .const import GROUP_ID_ADMIN
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
from .providers import auth_provider_from_config, AuthProvider, LoginFlow from .providers import auth_provider_from_config, AuthProvider, LoginFlow
@ -117,6 +118,10 @@ class AuthManager:
"""Retrieve a user.""" """Retrieve a user."""
return await self._store.async_get_user(user_id) return await self._store.async_get_user(user_id)
async def async_get_group(self, group_id: str) -> Optional[models.Group]:
"""Retrieve all groups."""
return await self._store.async_get_group(group_id)
async def async_get_user_by_credentials( async def async_get_user_by_credentials(
self, credentials: models.Credentials) -> Optional[models.User]: self, credentials: models.Credentials) -> Optional[models.User]:
"""Get a user by credential, return None if not found.""" """Get a user by credential, return None if not found."""
@ -127,13 +132,15 @@ class AuthManager:
return None return None
async def async_create_system_user(self, name: str) -> models.User: async def async_create_system_user(
self, name: str,
group_ids: Optional[List[str]] = None) -> models.User:
"""Create a system user.""" """Create a system user."""
user = await self._store.async_create_user( user = await self._store.async_create_user(
name=name, name=name,
system_generated=True, system_generated=True,
is_active=True, is_active=True,
groups=[], group_ids=group_ids or [],
) )
self.hass.bus.async_fire(EVENT_USER_ADDED, { self.hass.bus.async_fire(EVENT_USER_ADDED, {
@ -144,11 +151,10 @@ class AuthManager:
async def async_create_user(self, name: str) -> models.User: async def async_create_user(self, name: str) -> models.User:
"""Create a user.""" """Create a user."""
group = (await self._store.async_get_groups())[0]
kwargs = { kwargs = {
'name': name, 'name': name,
'is_active': True, 'is_active': True,
'groups': [group] 'group_ids': [GROUP_ID_ADMIN]
} # type: Dict[str, Any] } # type: Dict[str, Any]
if await self._user_should_be_owner(): if await self._user_should_be_owner():
@ -213,6 +219,17 @@ class AuthManager:
'user_id': user.id 'user_id': user.id
}) })
async def async_update_user(self, user: models.User,
name: Optional[str] = None,
group_ids: Optional[List[str]] = None) -> None:
"""Update a user."""
kwargs = {} # type: Dict[str,Any]
if name is not None:
kwargs['name'] = name
if group_ids is not None:
kwargs['group_ids'] = group_ids
await self._store.async_update_user(user, **kwargs)
async def async_activate_user(self, user: models.User) -> None: async def async_activate_user(self, user: models.User) -> None:
"""Activate a user.""" """Activate a user."""
await self._store.async_activate_user(user) await self._store.async_activate_user(user)

View File

@ -10,11 +10,14 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import models from . import models
from .permissions import DEFAULT_POLICY from .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
from .permissions import system_policies
from .permissions.types import PolicyType # noqa: F401
STORAGE_VERSION = 1 STORAGE_VERSION = 1
STORAGE_KEY = 'auth' STORAGE_KEY = 'auth'
INITIAL_GROUP_NAME = 'All Access' GROUP_NAME_ADMIN = 'Administrators'
GROUP_NAME_READ_ONLY = 'Read Only'
class AuthStore: class AuthStore:
@ -42,6 +45,14 @@ class AuthStore:
return list(self._groups.values()) return list(self._groups.values())
async def async_get_group(self, group_id: str) -> Optional[models.Group]:
"""Retrieve all users."""
if self._groups is None:
await self._async_load()
assert self._groups is not None
return self._groups.get(group_id)
async def async_get_users(self) -> List[models.User]: async def async_get_users(self) -> List[models.User]:
"""Retrieve all users.""" """Retrieve all users."""
if self._users is None: if self._users is None:
@ -63,7 +74,7 @@ class AuthStore:
is_active: Optional[bool] = None, is_active: Optional[bool] = None,
system_generated: Optional[bool] = None, system_generated: Optional[bool] = None,
credentials: Optional[models.Credentials] = None, credentials: Optional[models.Credentials] = None,
groups: Optional[List[models.Group]] = None) -> models.User: group_ids: Optional[List[str]] = None) -> models.User:
"""Create a new user.""" """Create a new user."""
if self._users is None: if self._users is None:
await self._async_load() await self._async_load()
@ -71,11 +82,18 @@ class AuthStore:
assert self._users is not None assert self._users is not None
assert self._groups is not None assert self._groups is not None
groups = []
for group_id in (group_ids or []):
group = self._groups.get(group_id)
if group is None:
raise ValueError('Invalid group specified {}'.format(group_id))
groups.append(group)
kwargs = { kwargs = {
'name': name, 'name': name,
# Until we get group management, we just put everyone in the # Until we get group management, we just put everyone in the
# same group. # same group.
'groups': groups or [], 'groups': groups,
} # type: Dict[str, Any] } # type: Dict[str, Any]
if is_owner is not None: if is_owner is not None:
@ -115,6 +133,33 @@ class AuthStore:
self._users.pop(user.id) self._users.pop(user.id)
self._async_schedule_save() self._async_schedule_save()
async def async_update_user(
self, user: models.User, name: Optional[str] = None,
is_active: Optional[bool] = None,
group_ids: Optional[List[str]] = None) -> None:
"""Update a user."""
assert self._groups is not None
if group_ids is not None:
groups = []
for grid in group_ids:
group = self._groups.get(grid)
if group is None:
raise ValueError("Invalid group specified.")
groups.append(group)
user.groups = groups
user.invalidate_permission_cache()
for attr_name, value in (
('name', name),
('is_active', is_active),
):
if value is not None:
setattr(user, attr_name, value)
self._async_schedule_save()
async def async_activate_user(self, user: models.User) -> None: async def async_activate_user(self, user: models.User) -> None:
"""Activate a user.""" """Activate a user."""
user.is_active = True user.is_active = True
@ -238,38 +283,98 @@ class AuthStore:
users = OrderedDict() # type: Dict[str, models.User] users = OrderedDict() # type: Dict[str, models.User]
groups = OrderedDict() # type: Dict[str, models.Group] groups = OrderedDict() # type: Dict[str, models.Group]
# When creating objects we mention each attribute explicetely. This # Soft-migrating data as we load. We are going to make sure we have a
# read only group and an admin group. There are two states that we can
# migrate from:
# 1. Data from a recent version which has a single group without policy
# 2. Data from old version which has no groups
has_admin_group = False
has_read_only_group = False
group_without_policy = None
# When creating objects we mention each attribute explicitly. This
# prevents crashing if user rolls back HA version after a new property # prevents crashing if user rolls back HA version after a new property
# was added. # was added.
for group_dict in data.get('groups', []): for group_dict in data.get('groups', []):
policy = None # type: Optional[PolicyType]
if group_dict['id'] == GROUP_ID_ADMIN:
has_admin_group = True
name = GROUP_NAME_ADMIN
policy = system_policies.ADMIN_POLICY
system_generated = True
elif group_dict['id'] == GROUP_ID_READ_ONLY:
has_read_only_group = True
name = GROUP_NAME_READ_ONLY
policy = system_policies.READ_ONLY_POLICY
system_generated = True
else:
name = group_dict['name']
policy = group_dict.get('policy')
system_generated = False
# We don't want groups without a policy that are not system groups
# This is part of migrating from state 1
if policy is None:
group_without_policy = group_dict['id']
continue
groups[group_dict['id']] = models.Group( groups[group_dict['id']] = models.Group(
name=group_dict['name'],
id=group_dict['id'], id=group_dict['id'],
policy=group_dict.get('policy', DEFAULT_POLICY), name=name,
policy=policy,
system_generated=system_generated,
) )
migrate_group = None # If there are no groups, add all existing users to the admin group.
# This is part of migrating from state 2
migrate_users_to_admin_group = (not groups and
group_without_policy is None)
if not groups: # If we find a no_policy_group, we need to migrate all users to the
migrate_group = models.Group( # admin group. We only do this if there are no other groups, as is
name=INITIAL_GROUP_NAME, # the expected state. If not expected state, not marking people admin.
policy=DEFAULT_POLICY # This is part of migrating from state 1
) if groups and group_without_policy is not None:
groups[migrate_group.id] = migrate_group group_without_policy = None
# This is part of migrating from state 1 and 2
if not has_admin_group:
admin_group = _system_admin_group()
groups[admin_group.id] = admin_group
# This is part of migrating from state 1 and 2
if not has_read_only_group:
read_only_group = _system_read_only_group()
groups[read_only_group.id] = read_only_group
for user_dict in data['users']: for user_dict in data['users']:
# Collect the users group.
user_groups = []
for group_id in user_dict.get('group_ids', []):
# This is part of migrating from state 1
if group_id == group_without_policy:
group_id = GROUP_ID_ADMIN
user_groups.append(groups[group_id])
# This is part of migrating from state 2
if (not user_dict['system_generated'] and
migrate_users_to_admin_group):
user_groups.append(groups[GROUP_ID_ADMIN])
users[user_dict['id']] = models.User( users[user_dict['id']] = models.User(
name=user_dict['name'], name=user_dict['name'],
groups=[groups[group_id] for group_id groups=user_groups,
in user_dict.get('group_ids', [])],
id=user_dict['id'], id=user_dict['id'],
is_owner=user_dict['is_owner'], is_owner=user_dict['is_owner'],
is_active=user_dict['is_active'], is_active=user_dict['is_active'],
system_generated=user_dict['system_generated'], 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']: for cred_dict in data['credentials']:
users[cred_dict['user_id']].credentials.append(models.Credentials( users[cred_dict['user_id']].credentials.append(models.Credentials(
@ -356,11 +461,11 @@ class AuthStore:
groups = [] groups = []
for group in self._groups.values(): for group in self._groups.values():
g_dict = { g_dict = {
'name': group.name,
'id': group.id, 'id': group.id,
} # type: Dict[str, Any] } # type: Dict[str, Any]
if group.policy is not DEFAULT_POLICY: if group.id not in (GROUP_ID_READ_ONLY, GROUP_ID_ADMIN):
g_dict['name'] = group.name
g_dict['policy'] = group.policy g_dict['policy'] = group.policy
groups.append(g_dict) groups.append(g_dict)
@ -410,13 +515,29 @@ class AuthStore:
"""Set default values for auth store.""" """Set default values for auth store."""
self._users = OrderedDict() # type: Dict[str, models.User] self._users = OrderedDict() # type: Dict[str, models.User]
# Add default group groups = OrderedDict() # type: Dict[str, models.Group]
all_access_group = models.Group( admin_group = _system_admin_group()
name=INITIAL_GROUP_NAME, groups[admin_group.id] = admin_group
policy=DEFAULT_POLICY, read_only_group = _system_read_only_group()
groups[read_only_group.id] = read_only_group
self._groups = groups
def _system_admin_group() -> models.Group:
"""Create system admin group."""
return models.Group(
name=GROUP_NAME_ADMIN,
id=GROUP_ID_ADMIN,
policy=system_policies.ADMIN_POLICY,
system_generated=True,
) )
groups = OrderedDict() # type: Dict[str, models.Group]
groups[all_access_group.id] = all_access_group
self._groups = groups def _system_read_only_group() -> models.Group:
"""Create read only group."""
return models.Group(
name=GROUP_NAME_READ_ONLY,
id=GROUP_ID_READ_ONLY,
policy=system_policies.READ_ONLY_POLICY,
system_generated=True,
)

View File

@ -3,3 +3,6 @@ from datetime import timedelta
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30) ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
MFA_SESSION_EXPIRATION = timedelta(minutes=5) MFA_SESSION_EXPIRATION = timedelta(minutes=5)
GROUP_ID_ADMIN = 'system-admin'
GROUP_ID_READ_ONLY = 'system-read-only'

View File

@ -8,6 +8,7 @@ import attr
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import permissions as perm_mdl from . import permissions as perm_mdl
from .const import GROUP_ID_ADMIN
from .util import generate_secret from .util import generate_secret
TOKEN_TYPE_NORMAL = 'normal' TOKEN_TYPE_NORMAL = 'normal'
@ -22,6 +23,7 @@ class Group:
name = attr.ib(type=str) # type: Optional[str] name = attr.ib(type=str) # type: Optional[str]
policy = attr.ib(type=perm_mdl.PolicyType) policy = attr.ib(type=perm_mdl.PolicyType)
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
system_generated = attr.ib(type=bool, default=False)
@attr.s(slots=True) @attr.s(slots=True)
@ -47,7 +49,7 @@ class User:
) # type: Dict[str, RefreshToken] ) # type: Dict[str, RefreshToken]
_permissions = attr.ib( _permissions = attr.ib(
type=perm_mdl.PolicyPermissions, type=Optional[perm_mdl.PolicyPermissions],
init=False, init=False,
cmp=False, cmp=False,
default=None, default=None,
@ -68,6 +70,19 @@ class User:
return self._permissions return self._permissions
@property
def is_admin(self) -> bool:
"""Return if user is part of the admin group."""
if self.is_owner:
return True
return self.is_active and any(
gr.id == GROUP_ID_ADMIN for gr in self.groups)
def invalidate_permission_cache(self) -> None:
"""Invalidate permission cache."""
self._permissions = None
@attr.s(slots=True) @attr.s(slots=True)
class RefreshToken: class RefreshToken:

View File

@ -5,20 +5,11 @@ from typing import ( # noqa: F401
import voluptuous as vol import voluptuous as vol
from homeassistant.core import State from .const import CAT_ENTITIES
from .types import PolicyType
from .common import CategoryType, PolicyType
from .entities import ENTITY_POLICY_SCHEMA, compile_entities from .entities import ENTITY_POLICY_SCHEMA, compile_entities
from .merge import merge_policies # noqa from .merge import merge_policies # noqa
# Default policy if group has no policy applied.
DEFAULT_POLICY = {
"entities": True
} # type: PolicyType
CAT_ENTITIES = 'entities'
POLICY_SCHEMA = vol.Schema({ POLICY_SCHEMA = vol.Schema({
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
}) })
@ -29,13 +20,20 @@ _LOGGER = logging.getLogger(__name__)
class AbstractPermissions: class AbstractPermissions:
"""Default permissions class.""" """Default permissions class."""
def check_entity(self, entity_id: str, key: str) -> bool: _cached_entity_func = None
"""Test if we can access entity."""
def _entity_func(self) -> Callable[[str, str], bool]:
"""Return a function that can test entity access."""
raise NotImplementedError raise NotImplementedError
def filter_states(self, states: List[State]) -> List[State]: def check_entity(self, entity_id: str, key: str) -> bool:
"""Filter a list of states for what the user is allowed to see.""" """Check if we can access entity."""
raise NotImplementedError entity_func = self._cached_entity_func
if entity_func is None:
entity_func = self._cached_entity_func = self._entity_func()
return entity_func(entity_id, key)
class PolicyPermissions(AbstractPermissions): class PolicyPermissions(AbstractPermissions):
@ -44,34 +42,10 @@ class PolicyPermissions(AbstractPermissions):
def __init__(self, policy: PolicyType) -> None: def __init__(self, policy: PolicyType) -> None:
"""Initialize the permission class.""" """Initialize the permission class."""
self._policy = policy self._policy = policy
self._compiled = {} # type: Dict[str, Callable[..., bool]]
def check_entity(self, entity_id: str, key: str) -> bool: def _entity_func(self) -> Callable[[str, str], bool]:
"""Test if we can access entity.""" """Return a function that can test entity access."""
func = self._policy_func(CAT_ENTITIES, compile_entities) return compile_entities(self._policy.get(CAT_ENTITIES))
return func(entity_id, (key,))
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))
_LOGGER.debug("Compiled %s func: %s", category, func)
return func
def __eq__(self, other: Any) -> bool: def __eq__(self, other: Any) -> bool:
"""Equals check.""" """Equals check."""
@ -85,13 +59,9 @@ class _OwnerPermissions(AbstractPermissions):
# pylint: disable=no-self-use # pylint: disable=no-self-use
def check_entity(self, entity_id: str, key: str) -> bool: def _entity_func(self) -> Callable[[str, str], bool]:
"""Test if we can access entity.""" """Return a function that can test entity access."""
return True return lambda entity_id, key: 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 OwnerPermissions = _OwnerPermissions() # pylint: disable=invalid-name

View File

@ -0,0 +1,7 @@
"""Permission constants."""
CAT_ENTITIES = 'entities'
SUBCAT_ALL = 'all'
POLICY_READ = 'read'
POLICY_CONTROL = 'control'
POLICY_EDIT = 'edit'

View File

@ -5,12 +5,8 @@ from typing import ( # noqa: F401
import voluptuous as vol import voluptuous as vol
from .common import CategoryType, ValueType, SUBCAT_ALL from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT
from .types import CategoryType, ValueType
POLICY_READ = 'read'
POLICY_CONTROL = 'control'
POLICY_EDIT = 'edit'
SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({ SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
vol.Optional(POLICY_READ): True, vol.Optional(POLICY_READ): True,
@ -32,28 +28,28 @@ ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({
})) }))
def _entity_allowed(schema: ValueType, keys: Tuple[str]) \ def _entity_allowed(schema: ValueType, key: str) \
-> Union[bool, None]: -> Union[bool, None]:
"""Test if an entity is allowed based on the keys.""" """Test if an entity is allowed based on the keys."""
if schema is None or isinstance(schema, bool): if schema is None or isinstance(schema, bool):
return schema return schema
assert isinstance(schema, dict) assert isinstance(schema, dict)
return schema.get(keys[0]) return schema.get(key)
def compile_entities(policy: CategoryType) \ def compile_entities(policy: CategoryType) \
-> Callable[[str, Tuple[str]], bool]: -> Callable[[str, str], bool]:
"""Compile policy into a function that tests policy.""" """Compile policy into a function that tests policy."""
# None, Empty Dict, False # None, Empty Dict, False
if not policy: if not policy:
def apply_policy_deny_all(entity_id: str, keys: Tuple[str]) -> bool: def apply_policy_deny_all(entity_id: str, key: str) -> bool:
"""Decline all.""" """Decline all."""
return False return False
return apply_policy_deny_all return apply_policy_deny_all
if policy is True: if policy is True:
def apply_policy_allow_all(entity_id: str, keys: Tuple[str]) -> bool: def apply_policy_allow_all(entity_id: str, key: str) -> bool:
"""Approve all.""" """Approve all."""
return True return True
@ -65,7 +61,7 @@ def compile_entities(policy: CategoryType) \
entity_ids = policy.get(ENTITY_ENTITY_IDS) entity_ids = policy.get(ENTITY_ENTITY_IDS)
all_entities = policy.get(SUBCAT_ALL) all_entities = policy.get(SUBCAT_ALL)
funcs = [] # type: List[Callable[[str, Tuple[str]], Union[None, bool]]] funcs = [] # type: List[Callable[[str, str], Union[None, bool]]]
# The order of these functions matter. The more precise are at the top. # 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 None, they cannot handle it.
@ -74,23 +70,23 @@ def compile_entities(policy: CategoryType) \
# Setting entity_ids to a boolean is final decision for permissions # Setting entity_ids to a boolean is final decision for permissions
# So return right away. # So return right away.
if isinstance(entity_ids, bool): if isinstance(entity_ids, bool):
def allowed_entity_id_bool(entity_id: str, keys: Tuple[str]) -> bool: def allowed_entity_id_bool(entity_id: str, key: str) -> bool:
"""Test if allowed entity_id.""" """Test if allowed entity_id."""
return entity_ids # type: ignore return entity_ids # type: ignore
return allowed_entity_id_bool return allowed_entity_id_bool
if entity_ids is not None: if entity_ids is not None:
def allowed_entity_id_dict(entity_id: str, keys: Tuple[str]) \ def allowed_entity_id_dict(entity_id: str, key: str) \
-> Union[None, bool]: -> Union[None, bool]:
"""Test if allowed entity_id.""" """Test if allowed entity_id."""
return _entity_allowed( return _entity_allowed(
entity_ids.get(entity_id), keys) # type: ignore entity_ids.get(entity_id), key) # type: ignore
funcs.append(allowed_entity_id_dict) funcs.append(allowed_entity_id_dict)
if isinstance(domains, bool): if isinstance(domains, bool):
def allowed_domain_bool(entity_id: str, keys: Tuple[str]) \ def allowed_domain_bool(entity_id: str, key: str) \
-> Union[None, bool]: -> Union[None, bool]:
"""Test if allowed domain.""" """Test if allowed domain."""
return domains return domains
@ -98,31 +94,31 @@ def compile_entities(policy: CategoryType) \
funcs.append(allowed_domain_bool) funcs.append(allowed_domain_bool)
elif domains is not None: elif domains is not None:
def allowed_domain_dict(entity_id: str, keys: Tuple[str]) \ def allowed_domain_dict(entity_id: str, key: str) \
-> Union[None, bool]: -> Union[None, bool]:
"""Test if allowed domain.""" """Test if allowed domain."""
domain = entity_id.split(".", 1)[0] domain = entity_id.split(".", 1)[0]
return _entity_allowed(domains.get(domain), keys) # type: ignore return _entity_allowed(domains.get(domain), key) # type: ignore
funcs.append(allowed_domain_dict) funcs.append(allowed_domain_dict)
if isinstance(all_entities, bool): if isinstance(all_entities, bool):
def allowed_all_entities_bool(entity_id: str, keys: Tuple[str]) \ def allowed_all_entities_bool(entity_id: str, key: str) \
-> Union[None, bool]: -> Union[None, bool]:
"""Test if allowed domain.""" """Test if allowed domain."""
return all_entities return all_entities
funcs.append(allowed_all_entities_bool) funcs.append(allowed_all_entities_bool)
elif all_entities is not None: elif all_entities is not None:
def allowed_all_entities_dict(entity_id: str, keys: Tuple[str]) \ def allowed_all_entities_dict(entity_id: str, key: str) \
-> Union[None, bool]: -> Union[None, bool]:
"""Test if allowed domain.""" """Test if allowed domain."""
return _entity_allowed(all_entities, keys) return _entity_allowed(all_entities, key)
funcs.append(allowed_all_entities_dict) funcs.append(allowed_all_entities_dict)
# Can happen if no valid subcategories specified # Can happen if no valid subcategories specified
if not funcs: if not funcs:
def apply_policy_deny_all_2(entity_id: str, keys: Tuple[str]) -> bool: def apply_policy_deny_all_2(entity_id: str, key: str) -> bool:
"""Decline all.""" """Decline all."""
return False return False
@ -132,16 +128,16 @@ def compile_entities(policy: CategoryType) \
func = funcs[0] func = funcs[0]
@wraps(func) @wraps(func)
def apply_policy_func(entity_id: str, keys: Tuple[str]) -> bool: def apply_policy_func(entity_id: str, key: str) -> bool:
"""Apply a single policy function.""" """Apply a single policy function."""
return func(entity_id, keys) is True return func(entity_id, key) is True
return apply_policy_func return apply_policy_func
def apply_policy_funcs(entity_id: str, keys: Tuple[str]) -> bool: def apply_policy_funcs(entity_id: str, key: str) -> bool:
"""Apply several policy functions.""" """Apply several policy functions."""
for func in funcs: for func in funcs:
result = func(entity_id, keys) result = func(entity_id, key)
if result is not None: if result is not None:
return result return result
return False return False

View File

@ -2,7 +2,7 @@
from typing import ( # noqa: F401 from typing import ( # noqa: F401
cast, Dict, List, Set) cast, Dict, List, Set)
from .common import PolicyType, CategoryType from .types import PolicyType, CategoryType
def merge_policies(policies: List[PolicyType]) -> PolicyType: def merge_policies(policies: List[PolicyType]) -> PolicyType:

View File

@ -0,0 +1,14 @@
"""System policies."""
from .const import CAT_ENTITIES, SUBCAT_ALL, POLICY_READ
ADMIN_POLICY = {
CAT_ENTITIES: True,
}
READ_ONLY_POLICY = {
CAT_ENTITIES: {
SUBCAT_ALL: {
POLICY_READ: True
}
}
}

View File

@ -29,5 +29,3 @@ CategoryType = Union[
# Example: { entities: … } # Example: { entities: … }
PolicyType = Mapping[str, CategoryType] PolicyType = Mapping[str, CategoryType]
SUBCAT_ALL = 'all'

View File

@ -13,9 +13,10 @@ from homeassistant.const import (
CONF_PENDING_TIME, CONF_TRIGGER_TIME) CONF_PENDING_TIME, CONF_TRIGGER_TIME)
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Demo alarm control panel platform.""" """Set up the Demo alarm control panel platform."""
add_entities([ async_add_entities([
manual.ManualAlarm(hass, 'Alarm', '1234', None, False, { manual.ManualAlarm(hass, 'Alarm', '1234', None, False, {
STATE_ALARM_ARMED_AWAY: { STATE_ALARM_ARMED_AWAY: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0), CONF_DELAY_TIME: datetime.timedelta(seconds=0),

View File

@ -12,10 +12,10 @@ import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyialarm==0.2'] REQUIREMENTS = ['pyialarm==0.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -89,6 +89,8 @@ class IAlarmPanel(alarm.AlarmControlPanel):
state = STATE_ALARM_ARMED_AWAY state = STATE_ALARM_ARMED_AWAY
elif status == self._client.ARMED_STAY: elif status == self._client.ARMED_STAY:
state = STATE_ALARM_ARMED_HOME state = STATE_ALARM_ARMED_HOME
elif status == self._client.TRIGGERED:
state = STATE_ALARM_TRIGGERED
else: else:
state = None state = None

View File

@ -0,0 +1,67 @@
"""
This component provides HA alarm_control_panel support for Lupusec System.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.lupusec/
"""
from datetime import timedelta
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.components.lupusec import DOMAIN as LUPUSEC_DOMAIN
from homeassistant.components.lupusec import LupusecDevice
from homeassistant.const import (STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED)
DEPENDENCIES = ['lupusec']
ICON = 'mdi:security'
SCAN_INTERVAL = timedelta(seconds=2)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up an alarm control panel for a Lupusec device."""
if discovery_info is None:
return
data = hass.data[LUPUSEC_DOMAIN]
alarm_devices = [LupusecAlarm(data, data.lupusec.get_alarm())]
add_entities(alarm_devices)
class LupusecAlarm(LupusecDevice, AlarmControlPanel):
"""An alarm_control_panel implementation for Lupusec."""
@property
def icon(self):
"""Return the icon."""
return ICON
@property
def state(self):
"""Return the state of the device."""
if self._device.is_standby:
state = STATE_ALARM_DISARMED
elif self._device.is_away:
state = STATE_ALARM_ARMED_AWAY
elif self._device.is_home:
state = STATE_ALARM_ARMED_HOME
else:
state = None
return state
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._device.set_away()
def alarm_disarm(self, code=None):
"""Send disarm command."""
self._device.set_standby()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self._device.set_home()

View File

@ -335,11 +335,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
return state_attr return state_attr
def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe to MQTT events. """Subscribe to MQTT events."""
This method must be run in the event loop and returns a coroutine.
"""
async_track_state_change( async_track_state_change(
self.hass, self.entity_id, self._async_state_changed_listener self.hass, self.entity_id, self._async_state_changed_listener
) )
@ -359,7 +356,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
_LOGGER.warning("Received unexpected payload: %s", payload) _LOGGER.warning("Received unexpected payload: %s", payload)
return return
return mqtt.async_subscribe( await mqtt.async_subscribe(
self.hass, self._command_topic, message_received, self._qos) self.hass, self._command_topic, message_received, self._qos)
async def _async_state_changed_listener(self, entity_id, old_state, async def _async_state_changed_listener(self, entity_id, old_state,

View File

@ -18,7 +18,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_CUSTOM_BYPASS) STATE_ALARM_ARMED_CUSTOM_BYPASS)
REQUIREMENTS = ['total_connect_client==0.20'] REQUIREMENTS = ['total_connect_client==0.22']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -16,9 +16,9 @@ from homeassistant.components import (
input_boolean, light, lock, media_player, scene, script, sensor, switch) input_boolean, light, lock, media_player, scene, script, sensor, switch)
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, SERVICE_LOCK, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CLOUD_NEVER_EXPOSED_ENTITIES,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, CONF_NAME, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON, STATE_UNLOCKED, SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON, STATE_UNLOCKED,
TEMP_CELSIUS, TEMP_FAHRENHEIT) TEMP_CELSIUS, TEMP_FAHRENHEIT)
@ -474,6 +474,26 @@ class _AlexaColorController(_AlexaInterface):
def name(self): def name(self):
return 'Alexa.ColorController' return 'Alexa.ColorController'
def properties_supported(self):
return [{'name': 'color'}]
def properties_retrievable(self):
return True
def get_property(self, name):
if name != 'color':
raise _UnsupportedProperty(name)
hue, saturation = self.entity.attributes.get(
light.ATTR_HS_COLOR, (0, 0))
return {
'hue': hue,
'saturation': saturation / 100.0,
'brightness': self.entity.attributes.get(
light.ATTR_BRIGHTNESS, 0) / 255.0,
}
class _AlexaColorTemperatureController(_AlexaInterface): class _AlexaColorTemperatureController(_AlexaInterface):
"""Implements Alexa.ColorTemperatureController. """Implements Alexa.ColorTemperatureController.
@ -717,6 +737,9 @@ class _ClimateCapabilities(_AlexaEntity):
return [_DisplayCategory.THERMOSTAT] return [_DisplayCategory.THERMOSTAT]
def interfaces(self): def interfaces(self):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & climate.SUPPORT_ON_OFF:
yield _AlexaPowerController(self.entity)
yield _AlexaThermostatController(self.hass, self.entity) yield _AlexaThermostatController(self.hass, self.entity)
yield _AlexaTemperatureSensor(self.hass, self.entity) yield _AlexaTemperatureSensor(self.hass, self.entity)
@ -1194,6 +1217,11 @@ async def async_api_discovery(hass, config, directive, context):
discovery_endpoints = [] discovery_endpoints = []
for entity in hass.states.async_all(): for entity in hass.states.async_all():
if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
_LOGGER.debug("Not exposing %s because it is never exposed",
entity.entity_id)
continue
if not config.should_expose(entity.entity_id): if not config.should_expose(entity.entity_id):
_LOGGER.debug("Not exposing %s because filtered by config", _LOGGER.debug("Not exposing %s because filtered by config",
entity.entity_id) entity.entity_id)
@ -1205,7 +1233,7 @@ async def async_api_discovery(hass, config, directive, context):
endpoint = { endpoint = {
'displayCategories': alexa_entity.display_categories(), 'displayCategories': alexa_entity.display_categories(),
'additionalApplianceDetails': {}, 'cookie': {},
'endpointId': alexa_entity.entity_id(), 'endpointId': alexa_entity.entity_id(),
'friendlyName': alexa_entity.friendly_name(), 'friendlyName': alexa_entity.friendly_name(),
'description': alexa_entity.description(), 'description': alexa_entity.description(),

View File

@ -20,7 +20,8 @@ from homeassistant.const import (
URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM,
URL_API_TEMPLATE, __version__) URL_API_TEMPLATE, __version__)
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.exceptions import TemplateError from homeassistant.auth.permissions.const import POLICY_READ
from homeassistant.exceptions import TemplateError, Unauthorized
from homeassistant.helpers import template from homeassistant.helpers import template
from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.state import AsyncTrackStates from homeassistant.helpers.state import AsyncTrackStates
@ -81,6 +82,8 @@ class APIEventStream(HomeAssistantView):
async def get(self, request): async def get(self, request):
"""Provide a streaming interface for the event bus.""" """Provide a streaming interface for the event bus."""
if not request['hass_user'].is_admin:
raise Unauthorized()
hass = request.app['hass'] hass = request.app['hass']
stop_obj = object() stop_obj = object()
to_write = asyncio.Queue(loop=hass.loop) to_write = asyncio.Queue(loop=hass.loop)
@ -185,7 +188,13 @@ class APIStatesView(HomeAssistantView):
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Get current states.""" """Get current states."""
return self.json(request.app['hass'].states.async_all()) user = request['hass_user']
entity_perm = user.permissions.check_entity
states = [
state for state in request.app['hass'].states.async_all()
if entity_perm(state.entity_id, 'read')
]
return self.json(states)
class APIEntityStateView(HomeAssistantView): class APIEntityStateView(HomeAssistantView):
@ -197,6 +206,10 @@ class APIEntityStateView(HomeAssistantView):
@ha.callback @ha.callback
def get(self, request, entity_id): def get(self, request, entity_id):
"""Retrieve state of entity.""" """Retrieve state of entity."""
user = request['hass_user']
if not user.permissions.check_entity(entity_id, POLICY_READ):
raise Unauthorized(entity_id=entity_id)
state = request.app['hass'].states.get(entity_id) state = request.app['hass'].states.get(entity_id)
if state: if state:
return self.json(state) return self.json(state)
@ -204,6 +217,8 @@ class APIEntityStateView(HomeAssistantView):
async def post(self, request, entity_id): async def post(self, request, entity_id):
"""Update state of entity.""" """Update state of entity."""
if not request['hass_user'].is_admin:
raise Unauthorized(entity_id=entity_id)
hass = request.app['hass'] hass = request.app['hass']
try: try:
data = await request.json() data = await request.json()
@ -236,6 +251,8 @@ class APIEntityStateView(HomeAssistantView):
@ha.callback @ha.callback
def delete(self, request, entity_id): def delete(self, request, entity_id):
"""Remove entity.""" """Remove entity."""
if not request['hass_user'].is_admin:
raise Unauthorized(entity_id=entity_id)
if request.app['hass'].states.async_remove(entity_id): if request.app['hass'].states.async_remove(entity_id):
return self.json_message("Entity removed.") return self.json_message("Entity removed.")
return self.json_message("Entity not found.", HTTP_NOT_FOUND) return self.json_message("Entity not found.", HTTP_NOT_FOUND)
@ -261,6 +278,8 @@ class APIEventView(HomeAssistantView):
async def post(self, request, event_type): async def post(self, request, event_type):
"""Fire events.""" """Fire events."""
if not request['hass_user'].is_admin:
raise Unauthorized()
body = await request.text() body = await request.text()
try: try:
event_data = json.loads(body) if body else None event_data = json.loads(body) if body else None
@ -346,6 +365,8 @@ class APITemplateView(HomeAssistantView):
async def post(self, request): async def post(self, request):
"""Render a template.""" """Render a template."""
if not request['hass_user'].is_admin:
raise Unauthorized()
try: try:
data = await request.json() data = await request.json()
tpl = template.Template(data['template'], request.app['hass']) tpl = template.Template(data['template'], request.app['hass'])
@ -363,6 +384,8 @@ class APIErrorLog(HomeAssistantView):
async def get(self, request): async def get(self, request):
"""Retrieve API error log.""" """Retrieve API error log."""
if not request['hass_user'].is_admin:
raise Unauthorized()
return web.FileResponse(request.app['hass'].data[DATA_LOGGING]) return web.FileResponse(request.app['hass'].data[DATA_LOGGING])

View File

@ -0,0 +1,68 @@
"""
Support for ASUSWRT devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/asuswrt/
"""
import logging
import voluptuous as vol
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_MODE,
CONF_PROTOCOL)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
REQUIREMENTS = ['aioasuswrt==1.1.11']
_LOGGER = logging.getLogger(__name__)
DOMAIN = "asuswrt"
DATA_ASUSWRT = DOMAIN
CONF_PUB_KEY = 'pub_key'
CONF_SSH_KEY = 'ssh_key'
CONF_REQUIRE_IP = 'require_ip'
DEFAULT_SSH_PORT = 22
SECRET_GROUP = 'Password or SSH Key'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']),
vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']),
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean,
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile
}),
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass, config):
"""Set up the asuswrt component."""
from aioasuswrt.asuswrt import AsusWrt
conf = config[DOMAIN]
api = AsusWrt(conf[CONF_HOST], conf.get(CONF_PORT),
conf.get(CONF_PROTOCOL) == 'telnet',
conf[CONF_USERNAME],
conf.get(CONF_PASSWORD, ''),
conf.get('ssh_key', conf.get('pub_key', '')),
conf.get(CONF_MODE), conf.get(CONF_REQUIRE_IP))
await api.connection.async_connect()
if not api.is_connected:
_LOGGER.error("Unable to setup asuswrt component")
return False
hass.data[DATA_ASUSWRT] = api
hass.async_create_task(async_load_platform(
hass, 'sensor', DOMAIN, {}, config))
hass.async_create_task(async_load_platform(
hass, 'device_tracker', DOMAIN, {}, config))
return True

View File

@ -11,8 +11,9 @@ import voluptuous as vol
from requests import RequestException from requests import RequestException
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback
from homeassistant.const import ( from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT) CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -20,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
_CONFIGURING = {} _CONFIGURING = {}
REQUIREMENTS = ['py-august==0.6.0'] REQUIREMENTS = ['py-august==0.7.0']
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
ACTIVITY_FETCH_LIMIT = 10 ACTIVITY_FETCH_LIMIT = 10
@ -116,7 +117,8 @@ def setup_august(hass, config, api, authenticator):
if DOMAIN in _CONFIGURING: if DOMAIN in _CONFIGURING:
hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN)) hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN))
hass.data[DATA_AUGUST] = AugustData(api, authentication.access_token) hass.data[DATA_AUGUST] = AugustData(
hass, api, authentication.access_token)
for component in AUGUST_COMPONENTS: for component in AUGUST_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config) discovery.load_platform(hass, component, DOMAIN, {}, config)
@ -136,9 +138,16 @@ def setup(hass, config):
"""Set up the August component.""" """Set up the August component."""
from august.api import Api from august.api import Api
from august.authenticator import Authenticator from august.authenticator import Authenticator
from requests import Session
conf = config[DOMAIN] conf = config[DOMAIN]
api = Api(timeout=conf.get(CONF_TIMEOUT)) try:
api_http_session = Session()
except RequestException as ex:
_LOGGER.warning("Creating HTTP session failed with: %s", str(ex))
api_http_session = None
api = Api(timeout=conf.get(CONF_TIMEOUT), http_session=api_http_session)
authenticator = Authenticator( authenticator = Authenticator(
api, api,
@ -154,8 +163,9 @@ def setup(hass, config):
class AugustData: class AugustData:
"""August data object.""" """August data object."""
def __init__(self, api, access_token): def __init__(self, hass, api, access_token):
"""Init August data object.""" """Init August data object."""
self._hass = hass
self._api = api self._api = api
self._access_token = access_token self._access_token = access_token
self._doorbells = self._api.get_doorbells(self._access_token) or [] self._doorbells = self._api.get_doorbells(self._access_token) or []
@ -168,6 +178,22 @@ class AugustData:
self._door_state_by_id = {} self._door_state_by_id = {}
self._activities_by_id = {} self._activities_by_id = {}
@callback
def august_api_stop(event):
"""Close the API HTTP session."""
_LOGGER.debug("Closing August HTTP session")
try:
self._api.http_session.close()
self._api.http_session = None
except RequestException:
pass
_LOGGER.debug("August HTTP session closed.")
self._hass.bus.listen_once(
EVENT_HOMEASSISTANT_STOP, august_api_stop)
_LOGGER.debug("Registered for HASS stop event")
@property @property
def house_ids(self): def house_ids(self):
"""Return a list of house_ids.""" """Return a list of house_ids."""
@ -201,8 +227,11 @@ class AugustData:
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT): def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
"""Update data object with latest from August API.""" """Update data object with latest from August API."""
_LOGGER.debug("Updating device activities") _LOGGER.debug("Start retrieving device activities")
for house_id in self.house_ids: for house_id in self.house_ids:
_LOGGER.debug("Updating device activity for house id %s",
house_id)
activities = self._api.get_house_activities(self._access_token, activities = self._api.get_house_activities(self._access_token,
house_id, house_id,
limit=limit) limit=limit)
@ -211,6 +240,7 @@ class AugustData:
for device_id in device_ids: for device_id in device_ids:
self._activities_by_id[device_id] = [a for a in activities if self._activities_by_id[device_id] = [a for a in activities if
a.device_id == device_id] a.device_id == device_id]
_LOGGER.debug("Completed retrieving device activities")
def get_doorbell_detail(self, doorbell_id): def get_doorbell_detail(self, doorbell_id):
"""Return doorbell detail.""" """Return doorbell detail."""
@ -223,7 +253,7 @@ class AugustData:
_LOGGER.debug("Start retrieving doorbell details") _LOGGER.debug("Start retrieving doorbell details")
for doorbell in self._doorbells: for doorbell in self._doorbells:
_LOGGER.debug("Updating status for %s", _LOGGER.debug("Updating doorbell status for %s",
doorbell.device_name) doorbell.device_name)
try: try:
detail_by_id[doorbell.device_id] =\ detail_by_id[doorbell.device_id] =\
@ -267,7 +297,7 @@ class AugustData:
_LOGGER.debug("Start retrieving door status") _LOGGER.debug("Start retrieving door status")
for lock in self._locks: for lock in self._locks:
_LOGGER.debug("Updating status for %s", _LOGGER.debug("Updating door status for %s",
lock.device_name) lock.device_name)
try: try:
@ -291,7 +321,7 @@ class AugustData:
_LOGGER.debug("Start retrieving locks status") _LOGGER.debug("Start retrieving locks status")
for lock in self._locks: for lock in self._locks:
_LOGGER.debug("Updating status for %s", _LOGGER.debug("Updating lock status for %s",
lock.device_name) lock.device_name)
try: try:
status_by_id[lock.device_id] = self._api.get_lock_status( status_by_id[lock.device_id] = self._api.get_lock_status(

View File

@ -0,0 +1,26 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "\u017d\u00e1dn\u00e9 oznamovac\u00ed slu\u017eby nejsou k dispozici."
},
"error": {
"invalid_code": "Neplatn\u00fd k\u00f3d, zkuste to znovu."
},
"step": {
"init": {
"description": "Vyberte pros\u00edm jednu z oznamovac\u00edch slu\u017eeb:",
"title": "Nastavte jednor\u00e1zov\u00e9 heslo dodan\u00e9 komponentou notify"
},
"setup": {
"title": "Ov\u011b\u0159en\u00ed nastaven\u00ed"
}
}
},
"totp": {
"error": {
"invalid_code": "Neplatn\u00fd k\u00f3d, zkuste to znovu. Pokud se tato chyba opakuje, ujist\u011bte se, \u017ee hodiny syst\u00e9mu Home Assistant jsou spr\u00e1vn\u011b nastaveny."
}
}
}
}

View File

@ -0,0 +1,35 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "No hay servicios de notificaci\u00f3n disponibles."
},
"error": {
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor int\u00e9ntelo de nuevo."
},
"step": {
"init": {
"description": "Seleccione uno de los servicios de notificaci\u00f3n:",
"title": "Configure una contrase\u00f1a de un solo uso entregada por el componente de notificaci\u00f3n"
},
"setup": {
"description": "Se ha enviado una contrase\u00f1a de un solo uso a trav\u00e9s de ** notificar. {notify_service} **. Por favor introd\u00facela a continuaci\u00f3n:",
"title": "Verificar la configuraci\u00f3n"
}
},
"title": "Notificar la contrase\u00f1a de un solo uso"
},
"totp": {
"error": {
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor int\u00e9ntalo de nuevo. Si recibes este error de forma consistente, por favor aseg\u00farate de que el reloj de tu Home Assistant es correcto."
},
"step": {
"init": {
"description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanea el c\u00f3digo QR con tu aplicaci\u00f3n de autenticaci\u00f3n. Si no tienes una, te recomendamos [Autenticador de Google] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \nDespu\u00e9s de escanear el c\u00f3digo, introduce el c\u00f3digo de seis d\u00edgitos de tu aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tienes problemas para escanear el c\u00f3digo QR, realiza una configuraci\u00f3n manual con el c\u00f3digo ** ` {code} ` **.",
"title": "Configure la autenticaci\u00f3n de dos factores utilizando TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@ -1,6 +1,27 @@
{ {
"mfa_setup": { "mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Nessun servizio di notifica disponibile."
},
"error": {
"invalid_code": "Codice non valido, per favore riprovare."
},
"step": {
"init": {
"description": "Selezionare uno dei servizi di notifica:"
},
"setup": {
"description": "\u00c8 stata inviata una password monouso tramite **notify.{notify_service}**. Per favore, inseriscila qui sotto:",
"title": "Verifica l'installazione"
}
},
"title": "Notifica la Password monouso"
},
"totp": { "totp": {
"error": {
"invalid_code": "Codice non valido, per favore riprovare. Se riscontri spesso questo errore, assicurati che l'orologio del sistema Home Assistant sia accurato."
},
"step": { "step": {
"init": { "init": {
"description": "Per attivare l'autenticazione a due fattori utilizzando password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \n Dopo aver scansionato il codice, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con codice ** ` {code} ` **.", "description": "Per attivare l'autenticazione a due fattori utilizzando password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \n Dopo aver scansionato il codice, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con codice ** ` {code} ` **.",

View File

@ -400,6 +400,9 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action):
This method is a coroutine. This method is a coroutine.
""" """
removes = [] removes = []
info = {
'name': name
}
for conf in trigger_configs: for conf in trigger_configs:
platform = await async_prepare_setup_platform( platform = await async_prepare_setup_platform(
@ -408,7 +411,7 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action):
if platform is None: if platform is None:
return None return None
remove = await platform.async_trigger(hass, conf, action) remove = await platform.async_trigger(hass, conf, action, info)
if not remove: if not remove:
_LOGGER.error("Error setting up trigger %s", name) _LOGGER.error("Error setting up trigger %s", name)

View File

@ -24,7 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
async def async_trigger(hass, config, action): async def async_trigger(hass, config, action, automation_info):
"""Listen for events based on configuration.""" """Listen for events based on configuration."""
event_type = config.get(CONF_EVENT_TYPE) event_type = config.get(CONF_EVENT_TYPE)
event_data_schema = vol.Schema( event_data_schema = vol.Schema(

View File

@ -33,7 +33,7 @@ def source_match(state, source):
return state and state.attributes.get('source') == source return state and state.attributes.get('source') == source
async def async_trigger(hass, config, action): async def async_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
source = config.get(CONF_SOURCE).lower() source = config.get(CONF_SOURCE).lower()
zone_entity_id = config.get(CONF_ZONE) zone_entity_id = config.get(CONF_ZONE)

View File

@ -22,7 +22,7 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
async def async_trigger(hass, config, action): async def async_trigger(hass, config, action, automation_info):
"""Listen for events based on configuration.""" """Listen for events based on configuration."""
event = config.get(CONF_EVENT) event = config.get(CONF_EVENT)

View File

@ -32,7 +32,7 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
async def async_trigger(hass, config, action): async def async_trigger(hass, config, action, automation_info):
"""Listen for events based on configuration.""" """Listen for events based on configuration."""
number = config.get(CONF_NUMBER) number = config.get(CONF_NUMBER)
held_more_than = config.get(CONF_HELD_MORE_THAN) held_more_than = config.get(CONF_HELD_MORE_THAN)

View File

@ -24,7 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
async def async_trigger(hass, config, action): async def async_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
topic = config.get(CONF_TOPIC) topic = config.get(CONF_TOPIC)
payload = config.get(CONF_PAYLOAD) payload = config.get(CONF_PAYLOAD)

View File

@ -28,7 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_trigger(hass, config, action): async def async_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
below = config.get(CONF_BELOW) below = config.get(CONF_BELOW)

View File

@ -26,7 +26,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
}), cv.key_dependency(CONF_FOR, CONF_TO)) }), cv.key_dependency(CONF_FOR, CONF_TO))
async def async_trigger(hass, config, action): async def async_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
from_state = config.get(CONF_FROM, MATCH_ALL) from_state = config.get(CONF_FROM, MATCH_ALL)

View File

@ -24,7 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
async def async_trigger(hass, config, action): async def async_trigger(hass, config, action, automation_info):
"""Listen for events based on configuration.""" """Listen for events based on configuration."""
event = config.get(CONF_EVENT) event = config.get(CONF_EVENT)
offset = config.get(CONF_OFFSET) offset = config.get(CONF_OFFSET)

View File

@ -22,7 +22,7 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
}) })
async def async_trigger(hass, config, action): async def async_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
value_template.hass = hass value_template.hass = hass

View File

@ -28,7 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT)) }), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT))
async def async_trigger(hass, config, action): async def async_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
if CONF_AT in config: if CONF_AT in config:
at_time = config.get(CONF_AT) at_time = config.get(CONF_AT)

View File

@ -14,6 +14,8 @@ from homeassistant.core import callback
from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from . import DOMAIN as AUTOMATION_DOMAIN
DEPENDENCIES = ('webhook',) DEPENDENCIES = ('webhook',)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -39,10 +41,11 @@ async def _handle_webhook(action, hass, webhook_id, request):
hass.async_run_job(action, {'trigger': result}) hass.async_run_job(action, {'trigger': result})
async def async_trigger(hass, config, action): async def async_trigger(hass, config, action, automation_info):
"""Trigger based on incoming webhooks.""" """Trigger based on incoming webhooks."""
webhook_id = config.get(CONF_WEBHOOK_ID) webhook_id = config.get(CONF_WEBHOOK_ID)
hass.components.webhook.async_register( hass.components.webhook.async_register(
AUTOMATION_DOMAIN, automation_info['name'],
webhook_id, partial(_handle_webhook, action)) webhook_id, partial(_handle_webhook, action))
@callback @callback

View File

@ -26,7 +26,7 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
async def async_trigger(hass, config, action): async def async_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
zone_entity_id = config.get(CONF_ZONE) zone_entity_id = config.get(CONF_ZONE)

View File

@ -6,8 +6,8 @@ https://home-assistant.io/components/binary_sensor.deconz/
""" """
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz.const import ( from homeassistant.components.deconz.const import (
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DECONZ_REACHABLE,
DECONZ_DOMAIN) DOMAIN as DECONZ_DOMAIN)
from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
@ -24,6 +24,8 @@ async def async_setup_platform(hass, config, async_add_entities,
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ binary sensor.""" """Set up the deCONZ binary sensor."""
gateway = hass.data[DECONZ_DOMAIN]
@callback @callback
def async_add_sensor(sensors): def async_add_sensor(sensors):
"""Add binary sensor from deCONZ.""" """Add binary sensor from deCONZ."""
@ -33,30 +35,35 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for sensor in sensors: for sensor in sensors:
if sensor.type in DECONZ_BINARY_SENSOR and \ if sensor.type in DECONZ_BINARY_SENSOR and \
not (not allow_clip_sensor and sensor.type.startswith('CLIP')): not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
entities.append(DeconzBinarySensor(sensor)) entities.append(DeconzBinarySensor(sensor, gateway))
async_add_entities(entities, True) async_add_entities(entities, True)
hass.data[DATA_DECONZ].listeners.append( gateway.listeners.append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
async_add_sensor(hass.data[DATA_DECONZ].api.sensors.values()) async_add_sensor(gateway.api.sensors.values())
class DeconzBinarySensor(BinarySensorDevice): class DeconzBinarySensor(BinarySensorDevice):
"""Representation of a binary sensor.""" """Representation of a binary sensor."""
def __init__(self, sensor): def __init__(self, sensor, gateway):
"""Set up sensor and add update callback to get data from websocket.""" """Set up sensor and add update callback to get data from websocket."""
self._sensor = sensor self._sensor = sensor
self.gateway = gateway
self.unsub_dispatcher = None
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe sensors events.""" """Subscribe sensors events."""
self._sensor.register_async_callback(self.async_update_callback) self._sensor.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \ self.gateway.deconz_ids[self.entity_id] = self._sensor.deconz_id
self._sensor.deconz_id self.unsub_dispatcher = async_dispatcher_connect(
self.hass, DECONZ_REACHABLE, self.async_update_callback)
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Disconnect sensor object when removed.""" """Disconnect sensor object when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()
self._sensor.remove_callback(self.async_update_callback) self._sensor.remove_callback(self.async_update_callback)
self._sensor = None self._sensor = None
@ -101,7 +108,7 @@ class DeconzBinarySensor(BinarySensorDevice):
@property @property
def available(self): def available(self):
"""Return True if sensor is available.""" """Return True if sensor is available."""
return self._sensor.reachable return self.gateway.available and self._sensor.reachable
@property @property
def should_poll(self): def should_poll(self):
@ -128,7 +135,7 @@ class DeconzBinarySensor(BinarySensorDevice):
self._sensor.uniqueid.count(':') != 7): self._sensor.uniqueid.count(':') != 7):
return None return None
serial = self._sensor.uniqueid.split('-', 1)[0] serial = self._sensor.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid bridgeid = self.gateway.api.config.bridgeid
return { return {
'connections': {(CONNECTION_ZIGBEE, serial)}, 'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)},

View File

@ -0,0 +1,74 @@
"""
Support for Fibaro binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.fibaro/
"""
import logging
from homeassistant.components.binary_sensor import (
BinarySensorDevice, ENTITY_ID_FORMAT)
from homeassistant.components.fibaro import (
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice)
DEPENDENCIES = ['fibaro']
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {
'com.fibaro.doorSensor': ['Door', 'mdi:window-open', 'door'],
'com.fibaro.windowSensor': ['Window', 'mdi:window-open', 'window'],
'com.fibaro.smokeSensor': ['Smoke', 'mdi:smoking', 'smoke'],
'com.fibaro.FGMS001': ['Motion', 'mdi:run', 'motion'],
'com.fibaro.heatDetector': ['Heat', 'mdi:fire', 'heat'],
}
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Perform the setup for Fibaro controller devices."""
if discovery_info is None:
return
add_entities(
[FibaroBinarySensor(device, hass.data[FIBARO_CONTROLLER])
for device in hass.data[FIBARO_DEVICES]['binary_sensor']], True)
class FibaroBinarySensor(FibaroDevice, BinarySensorDevice):
"""Representation of a Fibaro Binary Sensor."""
def __init__(self, fibaro_device, controller):
"""Initialize the binary_sensor."""
self._state = None
super().__init__(fibaro_device, controller)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
stype = None
if fibaro_device.type in SENSOR_TYPES:
stype = fibaro_device.type
elif fibaro_device.baseType in SENSOR_TYPES:
stype = fibaro_device.baseType
if stype:
self._device_class = SENSOR_TYPES[stype][2]
self._icon = SENSOR_TYPES[stype][1]
else:
self._device_class = None
self._icon = None
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return self._icon
@property
def device_class(self):
"""Return the device class of the sensor."""
return self._device_class
@property
def is_on(self):
"""Return true if sensor is on."""
return self._state
def update(self):
"""Get the latest data and update the state."""
self._state = self.current_binary_state

View File

@ -57,7 +57,8 @@ class InsteonBinarySensor(InsteonEntity, BinarySensorDevice):
"""Return the boolean response if the node is on.""" """Return the boolean response if the node is on."""
on_val = bool(self._insteon_device_state.value) on_val = bool(self._insteon_device_state.value)
if self._insteon_device_state.name == 'lightSensor': if self._insteon_device_state.name in ['lightSensor',
'openClosedSensor']:
return not on_val return not on_val
return on_val return on_val

View File

@ -0,0 +1,53 @@
"""
This component provides HA binary_sensor support for Lupusec Security System.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.lupusec/
"""
import logging
from datetime import timedelta
from homeassistant.components.lupusec import (LupusecDevice,
DOMAIN as LUPUSEC_DOMAIN)
from homeassistant.components.binary_sensor import (BinarySensorDevice,
DEVICE_CLASSES)
DEPENDENCIES = ['lupusec']
SCAN_INTERVAL = timedelta(seconds=2)
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a sensor for an Lupusec device."""
if discovery_info is None:
return
import lupupy.constants as CONST
data = hass.data[LUPUSEC_DOMAIN]
device_types = [CONST.TYPE_OPENING]
devices = []
for device in data.lupusec.get_devices(generic_type=device_types):
devices.append(LupusecBinarySensor(data, device))
add_entities(devices)
class LupusecBinarySensor(LupusecDevice, BinarySensorDevice):
"""A binary sensor implementation for Lupusec device."""
@property
def is_on(self):
"""Return True if the binary sensor is on."""
return self._device.is_on
@property
def device_class(self):
"""Return the class of the binary sensor."""
if self._device.generic_type not in DEVICE_CLASSES:
return None
return self._device.generic_type

View File

@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.mqtt/ https://home-assistant.io/components/binary_sensor.mqtt/
""" """
import logging import logging
from typing import Optional
import voluptuous as vol import voluptuous as vol
@ -19,7 +18,8 @@ from homeassistant.const import (
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo) MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
subscription)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -79,21 +79,8 @@ async def _async_setup_entity(hass, config, async_add_entities,
value_template.hass = hass value_template.hass = hass
async_add_entities([MqttBinarySensor( async_add_entities([MqttBinarySensor(
config.get(CONF_NAME), config,
config.get(CONF_STATE_TOPIC), discovery_hash
config.get(CONF_AVAILABILITY_TOPIC),
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,
)]) )])
@ -101,35 +88,71 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
MqttEntityDeviceInfo, BinarySensorDevice): MqttEntityDeviceInfo, BinarySensorDevice):
"""Representation a binary sensor that is updated by MQTT.""" """Representation a binary sensor that is updated by MQTT."""
def __init__(self, name, state_topic, availability_topic, device_class, def __init__(self, config, 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.""" """Initialize the MQTT binary sensor."""
MqttAvailability.__init__(self, availability_topic, qos, self._config = config
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
MqttEntityDeviceInfo.__init__(self, device_config)
self._name = name
self._state = None self._state = None
self._state_topic = state_topic self._sub_state = None
self._device_class = device_class
self._payload_on = payload_on
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 self._delay_listener = None
self._name = None
self._state_topic = None
self._device_class = None
self._payload_on = None
self._payload_off = None
self._qos = None
self._force_update = None
self._off_delay = None
self._template = None
self._unique_id = None
# Load config
self._setup_from_config(config)
availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
device_config = config.get(CONF_DEVICE)
MqttAvailability.__init__(self, availability_topic, self._qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash,
self.discovery_update)
MqttEntityDeviceInfo.__init__(self, device_config)
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe mqtt events.""" """Subscribe mqtt events."""
await MqttAvailability.async_added_to_hass(self) await MqttAvailability.async_added_to_hass(self)
await MqttDiscoveryUpdate.async_added_to_hass(self) await MqttDiscoveryUpdate.async_added_to_hass(self)
await self._subscribe_topics()
async def discovery_update(self, discovery_payload):
"""Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload)
self._setup_from_config(config)
await self.availability_discovery_update(config)
await self._subscribe_topics()
self.async_schedule_update_ha_state()
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
self._name = config.get(CONF_NAME)
self._state_topic = config.get(CONF_STATE_TOPIC)
self._device_class = config.get(CONF_DEVICE_CLASS)
self._qos = config.get(CONF_QOS)
self._force_update = config.get(CONF_FORCE_UPDATE)
self._off_delay = config.get(CONF_OFF_DELAY)
self._payload_on = config.get(CONF_PAYLOAD_ON)
self._payload_off = config.get(CONF_PAYLOAD_OFF)
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None and value_template.hass is None:
value_template.hass = self.hass
self._template = value_template
self._unique_id = config.get(CONF_UNIQUE_ID)
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@callback @callback
def off_delay_listener(now): def off_delay_listener(now):
"""Switch device off after a delay.""" """Switch device off after a delay."""
@ -163,8 +186,16 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
await mqtt.async_subscribe( self._sub_state = await subscription.async_subscribe_topics(
self.hass, self._state_topic, state_message_received, self._qos) self.hass, self._sub_state,
{'state_topic': {'topic': self._state_topic,
'msg_callback': state_message_received,
'qos': self._qos}})
async def async_will_remove_from_hass(self):
"""Unsubscribe when removed."""
await subscription.async_unsubscribe_topics(self.hass, self._sub_state)
await MqttAvailability.async_will_remove_from_hass(self)
@property @property
def should_poll(self): def should_poll(self):

View File

@ -0,0 +1,104 @@
"""
Support for Minut Point.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.point/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.point import MinutPointEntity
from homeassistant.components.point.const import (
DOMAIN as POINT_DOMAIN, NEW_DEVICE, SIGNAL_WEBHOOK)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
EVENTS = {
'battery': # On means low, Off means normal
('battery_low', ''),
'button_press': # On means the button was pressed, Off means normal
('short_button_press', ''),
'cold': # On means cold, Off means normal
('temperature_low', 'temperature_risen_normal'),
'connectivity': # On means connected, Off means disconnected
('device_online', 'device_offline'),
'dry': # On means too dry, Off means normal
('humidity_low', 'humidity_risen_normal'),
'heat': # On means hot, Off means normal
('temperature_high', 'temperature_dropped_normal'),
'moisture': # On means wet, Off means dry
('humidity_high', 'humidity_dropped_normal'),
'sound': # On means sound detected, Off means no sound (clear)
('avg_sound_high', 'sound_level_dropped_normal'),
'tamper': # On means the point was removed or attached
('tamper', ''),
}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a Point's binary sensors based on a config entry."""
device_id = config_entry.data[NEW_DEVICE]
client = hass.data[POINT_DOMAIN][config_entry.entry_id]
async_add_entities((MinutPointBinarySensor(client, device_id, device_class)
for device_class in EVENTS), True)
class MinutPointBinarySensor(MinutPointEntity, BinarySensorDevice):
"""The platform class required by Home Assistant."""
def __init__(self, point_client, device_id, device_class):
"""Initialize the entity."""
super().__init__(point_client, device_id, device_class)
self._async_unsub_hook_dispatcher_connect = None
self._events = EVENTS[device_class]
self._is_on = None
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
await super().async_added_to_hass()
self._async_unsub_hook_dispatcher_connect = async_dispatcher_connect(
self.hass, SIGNAL_WEBHOOK, self._webhook_event)
async def async_will_remove_from_hass(self):
"""Disconnect dispatcher listener when removed."""
await super().async_will_remove_from_hass()
if self._async_unsub_hook_dispatcher_connect:
self._async_unsub_hook_dispatcher_connect()
@callback
def _update_callback(self):
"""Update the value of the sensor."""
if not self.is_updated:
return
if self._events[0] in self.device.ongoing_events:
self._is_on = True
else:
self._is_on = None
self.async_schedule_update_ha_state()
@callback
def _webhook_event(self, data, webhook):
"""Process new event from the webhook."""
if self.device.webhook != webhook:
return
_type = data.get('event', {}).get('type')
if _type not in self._events:
return
_LOGGER.debug("Recieved webhook: %s", _type)
if _type == self._events[0]:
self._is_on = True
if _type == self._events[1]:
self._is_on = None
self.async_schedule_update_ha_state()
@property
def is_on(self):
"""Return the state of the binary sensor."""
if self.device_class == 'connectivity':
# connectivity is the other way around.
return not self._is_on
return self._is_on

View File

@ -8,28 +8,29 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.rainmachine import ( from homeassistant.components.rainmachine import (
BINARY_SENSORS, DATA_RAINMACHINE, SENSOR_UPDATE_TOPIC, TYPE_FREEZE, BINARY_SENSORS, DATA_CLIENT, DOMAIN as RAINMACHINE_DOMAIN,
TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH, SENSOR_UPDATE_TOPIC, TYPE_FREEZE, TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS,
TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity) TYPE_HOURLY, TYPE_MONTH, TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY,
from homeassistant.const import CONF_MONITORED_CONDITIONS RainMachineEntity)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['rainmachine'] DEPENDENCIES = ['rainmachine']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_platform( async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None): hass, config, async_add_entities, discovery_info=None):
"""Set up the RainMachine Switch platform.""" """Set up RainMachine binary sensors based on the old way."""
if discovery_info is None: pass
return
rainmachine = hass.data[DATA_RAINMACHINE]
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up RainMachine binary sensors based on a config entry."""
rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id]
binary_sensors = [] binary_sensors = []
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: for sensor_type in rainmachine.binary_sensor_conditions:
name, icon = BINARY_SENSORS[sensor_type] name, icon = BINARY_SENSORS[sensor_type]
binary_sensors.append( binary_sensors.append(
RainMachineBinarySensor(rainmachine, sensor_type, name, icon)) RainMachineBinarySensor(rainmachine, sensor_type, name, icon))
@ -70,15 +71,15 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
return '{0}_{1}'.format( return '{0}_{1}'.format(
self.rainmachine.device_mac.replace(':', ''), self._sensor_type) self.rainmachine.device_mac.replace(':', ''), self._sensor_type)
async def async_added_to_hass(self):
"""Register callbacks."""
@callback @callback
def _update_data(self): def update(self):
"""Update the state.""" """Update the state."""
self.async_schedule_update_ha_state(True) self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self): self._dispatcher_handlers.append(async_dispatcher_connect(
"""Register callbacks.""" self.hass, SENSOR_UPDATE_TOPIC, update))
async_dispatcher_connect(
self.hass, SENSOR_UPDATE_TOPIC, self._update_data)
async def async_update(self): async def async_update(self):
"""Update the state.""" """Update the state."""

View File

@ -60,7 +60,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
data = hass.data[SENSE_DATA] data = hass.data[SENSE_DATA]
sense_devices = data.get_discovered_device_data() sense_devices = data.get_discovered_device_data()
devices = [SenseDevice(data, device) for device in sense_devices] devices = [SenseDevice(data, device) for device in sense_devices
if device['tags']['DeviceListAllowed'] == 'true']
add_entities(devices) add_entities(devices)

View File

@ -58,10 +58,12 @@ async def async_setup_platform(hass, config, async_add_entities,
entity_ids = set() entity_ids = set()
manual_entity_ids = device_config.get(ATTR_ENTITY_ID) manual_entity_ids = device_config.get(ATTR_ENTITY_ID)
for template in ( invalid_templates = []
value_template,
icon_template, for tpl_name, template in (
entity_picture_template, (CONF_VALUE_TEMPLATE, value_template),
(CONF_ICON_TEMPLATE, icon_template),
(CONF_ENTITY_PICTURE_TEMPLATE, entity_picture_template),
): ):
if template is None: if template is None:
continue continue
@ -73,6 +75,8 @@ async def async_setup_platform(hass, config, async_add_entities,
template_entity_ids = template.extract_entities() template_entity_ids = template.extract_entities()
if template_entity_ids == MATCH_ALL: if template_entity_ids == MATCH_ALL:
entity_ids = MATCH_ALL entity_ids = MATCH_ALL
# Cut off _template from name
invalid_templates.append(tpl_name[:-9])
elif entity_ids != MATCH_ALL: elif entity_ids != MATCH_ALL:
entity_ids |= set(template_entity_ids) entity_ids |= set(template_entity_ids)
@ -81,6 +85,14 @@ async def async_setup_platform(hass, config, async_add_entities,
elif entity_ids != MATCH_ALL: elif entity_ids != MATCH_ALL:
entity_ids = list(entity_ids) entity_ids = list(entity_ids)
if invalid_templates:
_LOGGER.warning(
'Template binary sensor %s has no entity ids configured to'
' track nor were we able to extract the entities to track'
' from the %s template(s). This entity will only be able'
' to be updated manually.',
device, ', '.join(invalid_templates))
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
device_class = device_config.get(CONF_DEVICE_CLASS) device_class = device_config.get(CONF_DEVICE_CLASS)
delay_on = device_config.get(CONF_DELAY_ON) delay_on = device_config.get(CONF_DELAY_ON)
@ -132,10 +144,12 @@ class BinarySensorTemplate(BinarySensorDevice):
@callback @callback
def template_bsensor_startup(event): def template_bsensor_startup(event):
"""Update template on startup.""" """Update template on startup."""
if self._entities != MATCH_ALL:
# Track state change only for valid templates
async_track_state_change( async_track_state_change(
self.hass, self._entities, template_bsensor_state_listener) self.hass, self._entities, template_bsensor_state_listener)
self.hass.async_add_job(self.async_check_state) self.async_check_state()
self.hass.bus.async_listen_once( self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, template_bsensor_startup) EVENT_HOMEASSISTANT_START, template_bsensor_startup)
@ -233,3 +247,7 @@ class BinarySensorTemplate(BinarySensorDevice):
async_track_same_state( async_track_same_state(
self.hass, period, set_state, entity_ids=self._entities, self.hass, period, set_state, entity_ids=self._entities,
async_check_same_func=lambda *args: self._async_render() == state) async_check_same_func=lambda *args: self._async_render() == state)
async def async_update(self):
"""Force update of the state from the template."""
self.async_check_state()

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.helpers.event import async_track_state_change
from homeassistant.util import utcnow from homeassistant.util import utcnow
REQUIREMENTS = ['numpy==1.15.3'] REQUIREMENTS = ['numpy==1.15.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,132 @@
"""
Support for w800rf32 binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.w800rf32/
"""
import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.components.w800rf32 import (W800RF32_DEVICE)
from homeassistant.const import (CONF_DEVICE_CLASS, CONF_NAME, CONF_DEVICES)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import event as evt
from homeassistant.util import dt as dt_util
from homeassistant.helpers.dispatcher import (async_dispatcher_connect)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['w800rf32']
CONF_OFF_DELAY = 'off_delay'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES): {
cv.string: vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_OFF_DELAY):
vol.All(cv.time_period, cv.positive_timedelta)
})
},
}, extra=vol.ALLOW_EXTRA)
async def async_setup_platform(hass, config,
add_entities, discovery_info=None):
"""Set up the Binary Sensor platform to w800rf32."""
binary_sensors = []
# device_id --> "c1 or a3" X10 device. entity (type dictionary)
# --> name, device_class etc
for device_id, entity in config[CONF_DEVICES].items():
_LOGGER.debug("Add %s w800rf32.binary_sensor (class %s)",
entity[CONF_NAME], entity.get(CONF_DEVICE_CLASS))
device = W800rf32BinarySensor(
device_id, entity.get(CONF_NAME), entity.get(CONF_DEVICE_CLASS),
entity.get(CONF_OFF_DELAY))
binary_sensors.append(device)
add_entities(binary_sensors)
class W800rf32BinarySensor(BinarySensorDevice):
"""A representation of a w800rf32 binary sensor."""
def __init__(self, device_id, name, device_class=None, off_delay=None):
"""Initialize the w800rf32 sensor."""
self._signal = W800RF32_DEVICE.format(device_id)
self._name = name
self._device_class = device_class
self._off_delay = off_delay
self._state = False
self._delay_listener = None
@callback
def _off_delay_listener(self, now):
"""Switch device off after a delay."""
self._delay_listener = None
self.update_state(False)
@property
def name(self):
"""Return the device name."""
return self._name
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_class(self):
"""Return the sensor class."""
return self._device_class
@property
def is_on(self):
"""Return true if the sensor state is True."""
return self._state
@callback
def binary_sensor_update(self, event):
"""Call for control updates from the w800rf32 gateway."""
import W800rf32 as w800rf32mod
if not isinstance(event, w800rf32mod.W800rf32Event):
return
dev_id = event.device
command = event.command
_LOGGER.debug(
"BinarySensor update (Device ID: %s Command %s ...)",
dev_id, command)
# Update the w800rf32 device state
if command in ('On', 'Off'):
is_on = command == 'On'
self.update_state(is_on)
if (self.is_on and self._off_delay is not None and
self._delay_listener is None):
self._delay_listener = evt.async_track_point_in_time(
self.hass, self._off_delay_listener,
dt_util.utcnow() + self._off_delay)
def update_state(self, state):
"""Update the state of the device."""
self._state = state
self.async_schedule_update_ha_state()
async def async_added_to_hass(self):
"""Register update callback."""
async_dispatcher_connect(self.hass, self._signal,
self.binary_sensor_update)

View File

@ -209,7 +209,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
else: else:
self._should_poll = True self._should_poll = True
if self.entity_id is not None: if self.entity_id is not None:
self._hass.bus.fire('motion', { self._hass.bus.fire('xiaomi_aqara.motion', {
'entity_id': self.entity_id 'entity_id': self.entity_id
}) })
@ -417,7 +417,7 @@ class XiaomiButton(XiaomiBinarySensor):
_LOGGER.warning("Unsupported click_type detected: %s", value) _LOGGER.warning("Unsupported click_type detected: %s", value)
return False return False
self._hass.bus.fire('click', { self._hass.bus.fire('xiaomi_aqara.click', {
'entity_id': self.entity_id, 'entity_id': self.entity_id,
'click_type': click_type 'click_type': click_type
}) })
@ -453,14 +453,14 @@ class XiaomiCube(XiaomiBinarySensor):
def parse_data(self, data, raw_data): def parse_data(self, data, raw_data):
"""Parse data sent by gateway.""" """Parse data sent by gateway."""
if self._data_key in data: if self._data_key in data:
self._hass.bus.fire('cube_action', { self._hass.bus.fire('xiaomi_aqara.cube_action', {
'entity_id': self.entity_id, 'entity_id': self.entity_id,
'action_type': data[self._data_key] 'action_type': data[self._data_key]
}) })
self._last_action = data[self._data_key] self._last_action = data[self._data_key]
if 'rotate' in data: if 'rotate' in data:
self._hass.bus.fire('cube_action', { self._hass.bus.fire('xiaomi_aqara.cube_action', {
'entity_id': self.entity_id, 'entity_id': self.entity_id,
'action_type': 'rotate', 'action_type': 'rotate',
'action_value': float(data['rotate'].replace(",", ".")) 'action_value': float(data['rotate'].replace(",", "."))

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT) CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
REQUIREMENTS = ['blinkpy==0.10.1'] REQUIREMENTS = ['blinkpy==0.10.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -89,13 +89,12 @@ class MqttCamera(Camera):
"""Return a unique ID.""" """Return a unique ID."""
return self._unique_id return self._unique_id
@asyncio.coroutine async def async_added_to_hass(self):
def async_added_to_hass(self):
"""Subscribe MQTT events.""" """Subscribe MQTT events."""
@callback @callback
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
"""Handle new MQTT messages.""" """Handle new MQTT messages."""
self._last_image = payload self._last_image = payload
return mqtt.async_subscribe( await mqtt.async_subscribe(
self.hass, self._topic, message_received, self._qos, None) self.hass, self._topic, message_received, self._qos, None)

View File

@ -1,5 +1,15 @@
{ {
"config": { "config": {
"abort": {
"no_devices_found": "No se encontraron dispositivos de Google Cast en la red.",
"single_instance_allowed": "S\u00f3lo es necesaria una \u00fanica configuraci\u00f3n de Google Cast."
},
"step": {
"confirm": {
"description": "\u00bfQuieres configurar Google Cast?",
"title": "Google Cast"
}
},
"title": "Google Cast" "title": "Google Cast"
} }
} }

View File

@ -249,9 +249,11 @@ class ClimateDevice(Entity):
self.hass, self.target_temperature_low, self.temperature_unit, self.hass, self.target_temperature_low, self.temperature_unit,
self.precision) self.precision)
if self.current_humidity is not None:
data[ATTR_CURRENT_HUMIDITY] = self.current_humidity
if supported_features & SUPPORT_TARGET_HUMIDITY: if supported_features & SUPPORT_TARGET_HUMIDITY:
data[ATTR_HUMIDITY] = self.target_humidity data[ATTR_HUMIDITY] = self.target_humidity
data[ATTR_CURRENT_HUMIDITY] = self.current_humidity
if supported_features & SUPPORT_TARGET_HUMIDITY_LOW: if supported_features & SUPPORT_TARGET_HUMIDITY_LOW:
data[ATTR_MIN_HUMIDITY] = self.min_humidity data[ATTR_MIN_HUMIDITY] = self.min_humidity

View File

@ -22,7 +22,7 @@ from homeassistant.const import (
ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS) ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pydaikin==0.6'] REQUIREMENTS = ['pydaikin==0.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -82,7 +82,6 @@ class DaikinClimate(ClimateDevice):
from pydaikin import appliance from pydaikin import appliance
self._api = api self._api = api
self._force_refresh = False
self._list = { self._list = {
ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN), ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN),
ATTR_FAN_MODE: list( ATTR_FAN_MODE: list(
@ -102,19 +101,11 @@ class DaikinClimate(ClimateDevice):
self._supported_features = SUPPORT_TARGET_TEMPERATURE \ self._supported_features = SUPPORT_TARGET_TEMPERATURE \
| SUPPORT_OPERATION_MODE | SUPPORT_OPERATION_MODE
daikin_attr = HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE] if self._api.device.support_fan_mode:
if self._api.device.values.get(daikin_attr) is not None:
self._supported_features |= SUPPORT_FAN_MODE self._supported_features |= SUPPORT_FAN_MODE
else:
# even devices without support must have a default valid value
self._api.device.values[daikin_attr] = 'A'
daikin_attr = HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE] if self._api.device.support_swing_mode:
if self._api.device.values.get(daikin_attr) is not None:
self._supported_features |= SUPPORT_SWING_MODE self._supported_features |= SUPPORT_SWING_MODE
else:
# even devices without support must have a default valid value
self._api.device.values[daikin_attr] = '0'
def get(self, key): def get(self, key):
"""Retrieve device settings from API library cache.""" """Retrieve device settings from API library cache."""
@ -189,7 +180,6 @@ class DaikinClimate(ClimateDevice):
_LOGGER.error("Invalid temperature %s", value) _LOGGER.error("Invalid temperature %s", value)
if values: if values:
self._force_refresh = True
self._api.device.set(values) self._api.device.set(values)
@property @property
@ -270,5 +260,4 @@ class DaikinClimate(ClimateDevice):
def update(self): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
self._api.update(no_throttle=self._force_refresh) self._api.update()
self._force_refresh = False

View File

@ -17,7 +17,8 @@ from homeassistant.components.climate import (
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA) SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, ATTR_ENTITY_ID, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, ATTR_ENTITY_ID,
SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_UNKNOWN) SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_UNKNOWN, PRECISION_HALVES,
PRECISION_TENTHS, PRECISION_WHOLE)
from homeassistant.helpers import condition from homeassistant.helpers import condition
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change, async_track_time_interval) async_track_state_change, async_track_time_interval)
@ -43,6 +44,7 @@ CONF_HOT_TOLERANCE = 'hot_tolerance'
CONF_KEEP_ALIVE = 'keep_alive' CONF_KEEP_ALIVE = 'keep_alive'
CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode' CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode'
CONF_AWAY_TEMP = 'away_temp' CONF_AWAY_TEMP = 'away_temp'
CONF_PRECISION = 'precision'
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
SUPPORT_OPERATION_MODE) SUPPORT_OPERATION_MODE)
@ -63,7 +65,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
cv.time_period, cv.positive_timedelta), cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_INITIAL_OPERATION_MODE): vol.Optional(CONF_INITIAL_OPERATION_MODE):
vol.In([STATE_AUTO, STATE_OFF]), vol.In([STATE_AUTO, STATE_OFF]),
vol.Optional(CONF_AWAY_TEMP): vol.Coerce(float) vol.Optional(CONF_AWAY_TEMP): vol.Coerce(float),
vol.Optional(CONF_PRECISION): vol.In(
[PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]),
}) })
@ -83,11 +87,13 @@ async def async_setup_platform(hass, config, async_add_entities,
keep_alive = config.get(CONF_KEEP_ALIVE) keep_alive = config.get(CONF_KEEP_ALIVE)
initial_operation_mode = config.get(CONF_INITIAL_OPERATION_MODE) initial_operation_mode = config.get(CONF_INITIAL_OPERATION_MODE)
away_temp = config.get(CONF_AWAY_TEMP) away_temp = config.get(CONF_AWAY_TEMP)
precision = config.get(CONF_PRECISION)
async_add_entities([GenericThermostat( async_add_entities([GenericThermostat(
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
target_temp, ac_mode, min_cycle_duration, cold_tolerance, target_temp, ac_mode, min_cycle_duration, cold_tolerance,
hot_tolerance, keep_alive, initial_operation_mode, away_temp)]) hot_tolerance, keep_alive, initial_operation_mode, away_temp,
precision)])
class GenericThermostat(ClimateDevice): class GenericThermostat(ClimateDevice):
@ -96,7 +102,7 @@ class GenericThermostat(ClimateDevice):
def __init__(self, hass, name, heater_entity_id, sensor_entity_id, def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration, min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
cold_tolerance, hot_tolerance, keep_alive, cold_tolerance, hot_tolerance, keep_alive,
initial_operation_mode, away_temp): initial_operation_mode, away_temp, precision):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self.hass = hass self.hass = hass
self._name = name self._name = name
@ -109,6 +115,7 @@ class GenericThermostat(ClimateDevice):
self._initial_operation_mode = initial_operation_mode self._initial_operation_mode = initial_operation_mode
self._saved_target_temp = target_temp if target_temp is not None \ self._saved_target_temp = target_temp if target_temp is not None \
else away_temp else away_temp
self._temp_precision = precision
if self.ac_mode: if self.ac_mode:
self._current_operation = STATE_COOL self._current_operation = STATE_COOL
self._operation_list = [STATE_COOL, STATE_OFF] self._operation_list = [STATE_COOL, STATE_OFF]
@ -202,6 +209,13 @@ class GenericThermostat(ClimateDevice):
"""Return the name of the thermostat.""" """Return the name of the thermostat."""
return self._name return self._name
@property
def precision(self):
"""Return the precision of the system."""
if self._temp_precision is not None:
return self._temp_precision
return super().precision
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""

View File

@ -7,17 +7,16 @@ https://home-assistant.io/components/climate.homematic/
import logging import logging
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_AUTO, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, STATE_AUTO, STATE_MANUAL, SUPPORT_OPERATION_MODE,
ClimateDevice) SUPPORT_TARGET_TEMPERATURE, ClimateDevice)
from homeassistant.components.homematic import ( from homeassistant.components.homematic import (
ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT, HMDevice) ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT, HMDevice)
from homeassistant.const import ATTR_TEMPERATURE, STATE_UNKNOWN, TEMP_CELSIUS from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
DEPENDENCIES = ['homematic'] DEPENDENCIES = ['homematic']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
STATE_MANUAL = 'manual'
STATE_BOOST = 'boost' STATE_BOOST = 'boost'
STATE_COMFORT = 'comfort' STATE_COMFORT = 'comfort'
STATE_LOWERING = 'lowering' STATE_LOWERING = 'lowering'
@ -41,7 +40,7 @@ HM_HUMI_MAP = [
] ]
HM_CONTROL_MODE = 'CONTROL_MODE' HM_CONTROL_MODE = 'CONTROL_MODE'
HM_IP_CONTROL_MODE = 'SET_POINT_MODE' HMIP_CONTROL_MODE = 'SET_POINT_MODE'
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
@ -78,21 +77,17 @@ class HMThermostat(HMDevice, ClimateDevice):
if HM_CONTROL_MODE not in self._data: if HM_CONTROL_MODE not in self._data:
return None return None
set_point_mode = self._data.get('SET_POINT_MODE', -1)
control_mode = self._data.get('CONTROL_MODE', -1)
boost_mode = self._data.get('BOOST_MODE', False)
# boost mode is active # boost mode is active
if boost_mode: if self._data.get('BOOST_MODE', False):
return STATE_BOOST return STATE_BOOST
# HM ip etrv 2 uses the set_point_mode to say if its # HmIP uses the set_point_mode to say if its
# auto or manual # auto or manual
if not set_point_mode == -1: if HMIP_CONTROL_MODE in self._data:
code = set_point_mode code = self._data[HMIP_CONTROL_MODE]
# Other devices use the control_mode # Other devices use the control_mode
else: else:
code = control_mode code = self._data['CONTROL_MODE']
# get the name of the mode # get the name of the mode
name = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][code] name = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][code]
@ -101,12 +96,15 @@ class HMThermostat(HMDevice, ClimateDevice):
@property @property
def operation_list(self): def operation_list(self):
"""Return the list of available operation modes.""" """Return the list of available operation modes."""
op_list = [] # HMIP use set_point_mode for operation
if HMIP_CONTROL_MODE in self._data:
return [STATE_MANUAL, STATE_AUTO, STATE_BOOST]
# HM
op_list = []
for mode in self._hmdevice.ACTIONNODE: for mode in self._hmdevice.ACTIONNODE:
if mode in HM_STATE_MAP: if mode in HM_STATE_MAP:
op_list.append(HM_STATE_MAP.get(mode)) op_list.append(HM_STATE_MAP.get(mode))
return op_list return op_list
@property @property
@ -157,11 +155,11 @@ class HMThermostat(HMDevice, ClimateDevice):
def _init_data_struct(self): def _init_data_struct(self):
"""Generate a data dict (self._data) from the Homematic metadata.""" """Generate a data dict (self._data) from the Homematic metadata."""
self._state = next(iter(self._hmdevice.WRITENODE.keys())) self._state = next(iter(self._hmdevice.WRITENODE.keys()))
self._data[self._state] = STATE_UNKNOWN self._data[self._state] = None
if HM_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE or \ if HM_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE or \
HM_IP_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE: HMIP_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE:
self._data[HM_CONTROL_MODE] = STATE_UNKNOWN self._data[HM_CONTROL_MODE] = None
for node in self._hmdevice.SENSORNODE.keys(): for node in self._hmdevice.SENSORNODE.keys():
self._data[node] = STATE_UNKNOWN self._data[node] = None

View File

@ -88,6 +88,12 @@ class MelissaClimate(ClimateDevice):
if self._data: if self._data:
return self._data[self._api.TEMP] return self._data[self._api.TEMP]
@property
def current_humidity(self):
"""Return the current humidity value."""
if self._data:
return self._data[self._api.HUMIDITY]
@property @property
def target_temperature_step(self): def target_temperature_step(self):
"""Return the supported step of target temperature.""" """Return the supported step of target temperature."""
@ -113,7 +119,8 @@ class MelissaClimate(ClimateDevice):
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
if self._cur_settings is not None: if self._cur_settings is None:
return None
return self._cur_settings[self._api.TEMP] return self._cur_settings[self._api.TEMP]
@property @property

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
REQUIREMENTS = ['millheater==0.2.2'] REQUIREMENTS = ['millheater==0.2.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,8 +32,7 @@ MIN_TEMP = 5
SERVICE_SET_ROOM_TEMP = 'mill_set_room_temperature' SERVICE_SET_ROOM_TEMP = 'mill_set_room_temperature'
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
SUPPORT_FAN_MODE | SUPPORT_ON_OFF | SUPPORT_FAN_MODE)
SUPPORT_OPERATION_MODE)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
@ -92,12 +91,14 @@ class MillHeater(ClimateDevice):
@property @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
if self._heater.is_gen1:
return SUPPORT_FLAGS return SUPPORT_FLAGS
return SUPPORT_FLAGS | SUPPORT_ON_OFF | SUPPORT_OPERATION_MODE
@property @property
def available(self): def available(self):
"""Return True if entity is available.""" """Return True if entity is available."""
return self._heater.device_status == 0 # weird api choice return self._heater.available
@property @property
def unique_id(self): def unique_id(self):
@ -112,16 +113,18 @@ class MillHeater(ClimateDevice):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
if self._heater.room: res = {
room = self._heater.room.name
else:
room = "Independent device"
return {
"room": room,
"open_window": self._heater.open_window, "open_window": self._heater.open_window,
"heating": self._heater.is_heating, "heating": self._heater.is_heating,
"controlled_by_tibber": self._heater.tibber_control, "controlled_by_tibber": self._heater.tibber_control,
"heater_generation": 1 if self._heater.is_gen1 else 2,
} }
if self._heater.room:
res['room'] = self._heater.room.name
res['avg_room_temp'] = self._heater.room.avg_temp
else:
res['room'] = "Independent device"
return res
@property @property
def temperature_unit(self): def temperature_unit(self):
@ -156,6 +159,8 @@ class MillHeater(ClimateDevice):
@property @property
def is_on(self): def is_on(self):
"""Return true if heater is on.""" """Return true if heater is on."""
if self._heater.is_gen1:
return True
return self._heater.power_status == 1 return self._heater.power_status == 1
@property @property
@ -176,6 +181,8 @@ class MillHeater(ClimateDevice):
@property @property
def operation_list(self): def operation_list(self):
"""List of available operation modes.""" """List of available operation modes."""
if self._heater.is_gen1:
return None
return [STATE_HEAT, STATE_OFF] return [STATE_HEAT, STATE_OFF]
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
@ -210,7 +217,7 @@ class MillHeater(ClimateDevice):
"""Set operation mode.""" """Set operation mode."""
if operation_mode == STATE_HEAT: if operation_mode == STATE_HEAT:
await self.async_turn_on() await self.async_turn_on()
elif operation_mode == STATE_OFF: elif operation_mode == STATE_OFF and not self._heater.is_gen1:
await self.async_turn_off() await self.async_turn_off()
else: else:
_LOGGER.error("Unrecognized operation mode: %s", operation_mode) _LOGGER.error("Unrecognized operation mode: %s", operation_mode)

View File

@ -168,18 +168,14 @@ class NestThermostat(ClimateDevice):
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
if self._mode != NEST_MODE_HEAT_COOL and \ if self._mode not in (NEST_MODE_HEAT_COOL, STATE_ECO):
self._mode != STATE_ECO and \
not self.is_away_mode_on:
return self._target_temperature return self._target_temperature
return None return None
@property @property
def target_temperature_low(self): def target_temperature_low(self):
"""Return the lower bound temperature we try to reach.""" """Return the lower bound temperature we try to reach."""
if (self.is_away_mode_on or self._mode == STATE_ECO) and \ if self._mode == STATE_ECO:
self._eco_temperature[0]:
# eco_temperature is always a low, high tuple
return self._eco_temperature[0] return self._eco_temperature[0]
if self._mode == NEST_MODE_HEAT_COOL: if self._mode == NEST_MODE_HEAT_COOL:
return self._target_temperature[0] return self._target_temperature[0]
@ -188,9 +184,7 @@ class NestThermostat(ClimateDevice):
@property @property
def target_temperature_high(self): def target_temperature_high(self):
"""Return the upper bound temperature we try to reach.""" """Return the upper bound temperature we try to reach."""
if (self.is_away_mode_on or self._mode == STATE_ECO) and \ if self._mode == STATE_ECO:
self._eco_temperature[1]:
# eco_temperature is always a low, high tuple
return self._eco_temperature[1] return self._eco_temperature[1]
if self._mode == NEST_MODE_HEAT_COOL: if self._mode == NEST_MODE_HEAT_COOL:
return self._target_temperature[1] return self._target_temperature[1]

View File

@ -0,0 +1,74 @@
"""
Support for Velbus thermostat.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/climate.velbus/
"""
import logging
from homeassistant.components.climate import (
STATE_HEAT, SUPPORT_TARGET_TEMPERATURE, ClimateDevice)
from homeassistant.components.velbus import (
DOMAIN as VELBUS_DOMAIN, VelbusEntity)
from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['velbus']
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE)
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Set up the Velbus thermostat platform."""
if discovery_info is None:
return
sensors = []
for sensor in discovery_info:
module = hass.data[VELBUS_DOMAIN].get_module(sensor[0])
channel = sensor[1]
sensors.append(VelbusClimate(module, channel))
async_add_entities(sensors)
class VelbusClimate(VelbusEntity, ClimateDevice):
"""Representation of a Velbus thermostat."""
@property
def supported_features(self):
"""Return the list off supported features."""
return SUPPORT_FLAGS
@property
def temperature_unit(self):
"""Return the unit this state is expressed in."""
if self._module.get_unit(self._channel) == '°C':
return TEMP_CELSIUS
return TEMP_FAHRENHEIT
@property
def current_temperature(self):
"""Return the current temperature."""
return self._module.get_state(self._channel)
@property
def current_operation(self):
"""Return current operation."""
return STATE_HEAT
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._module.get_climate_target()
def set_temperature(self, **kwargs):
"""Set new target temperatures."""
temp = kwargs.get(ATTR_TEMPERATURE)
if temp is None:
return
self._module.set_temp(temp)
self.schedule_update_ha_state()

View File

@ -12,24 +12,20 @@ import os
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME) EVENT_HOMEASSISTANT_START, CLOUD_NEVER_EXPOSED_ENTITIES, CONF_REGION,
CONF_MODE, CONF_NAME)
from homeassistant.helpers import entityfilter, config_validation as cv from homeassistant.helpers import entityfilter, config_validation as cv
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import helpers as ga_h from homeassistant.components.google_assistant import helpers as ga_h
from homeassistant.components.google_assistant import const as ga_c from homeassistant.components.google_assistant import const as ga_c
from . import http_api, iot, auth_api from . import http_api, iot, auth_api, prefs
from .const import CONFIG_DIR, DOMAIN, SERVERS from .const import CONFIG_DIR, DOMAIN, SERVERS
REQUIREMENTS = ['warrant==0.6.1'] REQUIREMENTS = ['warrant==0.6.1']
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
STORAGE_ENABLE_ALEXA = 'alexa_enabled'
STORAGE_ENABLE_GOOGLE = 'google_enabled'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_UNDEF = object()
CONF_ALEXA = 'alexa' CONF_ALEXA = 'alexa'
CONF_ALIASES = 'aliases' CONF_ALIASES = 'aliases'
@ -68,7 +64,7 @@ ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({
}) })
GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend({ GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend({
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA} vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA},
}) })
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
@ -124,12 +120,11 @@ class Cloud:
self.alexa_config = alexa self.alexa_config = alexa
self.google_actions_user_conf = google_actions self.google_actions_user_conf = google_actions
self._gactions_config = None self._gactions_config = None
self._prefs = None self.prefs = prefs.CloudPreferences(hass)
self.id_token = None self.id_token = None
self.access_token = None self.access_token = None
self.refresh_token = None self.refresh_token = None
self.iot = iot.CloudIoT(self) self.iot = iot.CloudIoT(self)
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
if mode == MODE_DEV: if mode == MODE_DEV:
self.cognito_client_id = cognito_client_id self.cognito_client_id = cognito_client_id
@ -184,26 +179,20 @@ class Cloud:
def should_expose(entity): def should_expose(entity):
"""If an entity should be exposed.""" """If an entity should be exposed."""
if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False
return conf['filter'](entity.entity_id) return conf['filter'](entity.entity_id)
self._gactions_config = ga_h.Config( self._gactions_config = ga_h.Config(
should_expose=should_expose, should_expose=should_expose,
agent_user_id=self.claims['cognito:username'], agent_user_id=self.claims['cognito:username'],
entity_config=conf.get(CONF_ENTITY_CONFIG), entity_config=conf.get(CONF_ENTITY_CONFIG),
allow_unlock=self.prefs.google_allow_unlock,
) )
return self._gactions_config return self._gactions_config
@property
def alexa_enabled(self):
"""Return if Alexa is enabled."""
return self._prefs[STORAGE_ENABLE_ALEXA]
@property
def google_enabled(self):
"""Return if Google is enabled."""
return self._prefs[STORAGE_ENABLE_GOOGLE]
def path(self, *parts): def path(self, *parts):
"""Get config path inside cloud dir. """Get config path inside cloud dir.
@ -243,20 +232,6 @@ class Cloud:
async def async_start(self, _): async def async_start(self, _):
"""Start the cloud component.""" """Start the cloud component."""
prefs = await self._store.async_load()
if prefs is None:
prefs = {}
if self.mode not in prefs:
# Default to True if already logged in to make this not a
# breaking change.
enabled = await self.hass.async_add_executor_job(
os.path.isfile, self.user_info_path)
prefs = {
STORAGE_ENABLE_ALEXA: enabled,
STORAGE_ENABLE_GOOGLE: enabled,
}
self._prefs = prefs
def load_config(): def load_config():
"""Load config.""" """Load config."""
# Ensure config dir exists # Ensure config dir exists
@ -273,6 +248,8 @@ class Cloud:
info = await self.hass.async_add_job(load_config) info = await self.hass.async_add_job(load_config)
await self.prefs.async_initialize(bool(info))
if info is None: if info is None:
return return
@ -282,15 +259,6 @@ class Cloud:
self.hass.add_job(self.iot.connect()) self.hass.add_job(self.iot.connect())
async def update_preferences(self, *, google_enabled=_UNDEF,
alexa_enabled=_UNDEF):
"""Update user preferences."""
if google_enabled is not _UNDEF:
self._prefs[STORAGE_ENABLE_GOOGLE] = google_enabled
if alexa_enabled is not _UNDEF:
self._prefs[STORAGE_ENABLE_ALEXA] = alexa_enabled
await self._store.async_save(self._prefs)
def _decode_claims(self, token): # pylint: disable=no-self-use def _decode_claims(self, token): # pylint: disable=no-self-use
"""Decode the claims in a token.""" """Decode the claims in a token."""
from jose import jwt from jose import jwt

View File

@ -3,6 +3,10 @@ DOMAIN = 'cloud'
CONFIG_DIR = '.cloud' CONFIG_DIR = '.cloud'
REQUEST_TIMEOUT = 10 REQUEST_TIMEOUT = 10
PREF_ENABLE_ALEXA = 'alexa_enabled'
PREF_ENABLE_GOOGLE = 'google_enabled'
PREF_GOOGLE_ALLOW_UNLOCK = 'google_allow_unlock'
SERVERS = { SERVERS = {
'production': { 'production': {
'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u', 'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',

View File

@ -15,7 +15,9 @@ from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import smart_home as google_sh from homeassistant.components.google_assistant import smart_home as google_sh
from . import auth_api from . import auth_api
from .const import DOMAIN, REQUEST_TIMEOUT from .const import (
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
PREF_GOOGLE_ALLOW_UNLOCK)
from .iot import STATE_DISCONNECTED, STATE_CONNECTED from .iot import STATE_DISCONNECTED, STATE_CONNECTED
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,8 +32,9 @@ SCHEMA_WS_STATUS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
WS_TYPE_UPDATE_PREFS = 'cloud/update_prefs' WS_TYPE_UPDATE_PREFS = 'cloud/update_prefs'
SCHEMA_WS_UPDATE_PREFS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ SCHEMA_WS_UPDATE_PREFS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_UPDATE_PREFS, vol.Required('type'): WS_TYPE_UPDATE_PREFS,
vol.Optional('google_enabled'): bool, vol.Optional(PREF_ENABLE_GOOGLE): bool,
vol.Optional('alexa_enabled'): bool, vol.Optional(PREF_ENABLE_ALEXA): bool,
vol.Optional(PREF_GOOGLE_ALLOW_UNLOCK): bool,
}) })
@ -288,7 +291,7 @@ async def websocket_update_prefs(hass, connection, msg):
changes = dict(msg) changes = dict(msg)
changes.pop('id') changes.pop('id')
changes.pop('type') changes.pop('type')
await cloud.update_preferences(**changes) await cloud.prefs.async_update(**changes)
connection.send_message(websocket_api.result_message( connection.send_message(websocket_api.result_message(
msg['id'], {'success': True})) msg['id'], {'success': True}))
@ -308,10 +311,9 @@ def _account_data(cloud):
'logged_in': True, 'logged_in': True,
'email': claims['email'], 'email': claims['email'],
'cloud': cloud.iot.state, 'cloud': cloud.iot.state,
'google_enabled': cloud.google_enabled, 'prefs': cloud.prefs.as_dict(),
'google_entities': cloud.google_actions_user_conf['filter'].config, 'google_entities': cloud.google_actions_user_conf['filter'].config,
'google_domains': list(google_sh.DOMAIN_TO_GOOGLE_TYPES), 'google_domains': list(google_sh.DOMAIN_TO_GOOGLE_TYPES),
'alexa_enabled': cloud.alexa_enabled,
'alexa_entities': cloud.alexa_config.should_expose.config, 'alexa_entities': cloud.alexa_config.should_expose.config,
'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS), 'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS),
} }

View File

@ -229,7 +229,7 @@ def async_handle_alexa(hass, cloud, payload):
"""Handle an incoming IoT message for Alexa.""" """Handle an incoming IoT message for Alexa."""
result = yield from alexa.async_handle_message( result = yield from alexa.async_handle_message(
hass, cloud.alexa_config, payload, hass, cloud.alexa_config, payload,
enabled=cloud.alexa_enabled) enabled=cloud.prefs.alexa_enabled)
return result return result
@ -237,7 +237,7 @@ def async_handle_alexa(hass, cloud, payload):
@asyncio.coroutine @asyncio.coroutine
def async_handle_google_actions(hass, cloud, payload): def async_handle_google_actions(hass, cloud, payload):
"""Handle an incoming IoT message for Google Actions.""" """Handle an incoming IoT message for Google Actions."""
if not cloud.google_enabled: if not cloud.prefs.google_enabled:
return ga.turned_off_response(payload) return ga.turned_off_response(payload)
result = yield from ga.async_handle_message( result = yield from ga.async_handle_message(

View File

@ -0,0 +1,64 @@
"""Preference management for cloud."""
from .const import (
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
PREF_GOOGLE_ALLOW_UNLOCK)
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
_UNDEF = object()
class CloudPreferences:
"""Handle cloud preferences."""
def __init__(self, hass):
"""Initialize cloud prefs."""
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._prefs = None
async def async_initialize(self, logged_in):
"""Finish initializing the preferences."""
prefs = await self._store.async_load()
if prefs is None:
# Backwards compat: we enable alexa/google if already logged in
prefs = {
PREF_ENABLE_ALEXA: logged_in,
PREF_ENABLE_GOOGLE: logged_in,
PREF_GOOGLE_ALLOW_UNLOCK: False,
}
await self._store.async_save(prefs)
self._prefs = prefs
async def async_update(self, *, google_enabled=_UNDEF,
alexa_enabled=_UNDEF, google_allow_unlock=_UNDEF):
"""Update user preferences."""
for key, value in (
(PREF_ENABLE_GOOGLE, google_enabled),
(PREF_ENABLE_ALEXA, alexa_enabled),
(PREF_GOOGLE_ALLOW_UNLOCK, google_allow_unlock),
):
if value is not _UNDEF:
self._prefs[key] = value
await self._store.async_save(self._prefs)
def as_dict(self):
"""Return dictionary version."""
return self._prefs
@property
def alexa_enabled(self):
"""Return if Alexa is enabled."""
return self._prefs[PREF_ENABLE_ALEXA]
@property
def google_enabled(self):
"""Return if Google is enabled."""
return self._prefs[PREF_ENABLE_GOOGLE]
@property
def google_allow_unlock(self):
"""Return if Google is allowed to unlock locks."""
return self._prefs.get(PREF_GOOGLE_ALLOW_UNLOCK, False)

View File

@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'coinbase' DOMAIN = 'coinbase'
CONF_API_SECRET = 'api_secret' CONF_API_SECRET = 'api_secret'
CONF_ACCOUNT_CURRENCIES = 'account_balance_currencies'
CONF_EXCHANGE_CURRENCIES = 'exchange_rate_currencies' CONF_EXCHANGE_CURRENCIES = 'exchange_rate_currencies'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
@ -31,6 +32,8 @@ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_API_SECRET): cv.string, vol.Required(CONF_API_SECRET): cv.string,
vol.Optional(CONF_ACCOUNT_CURRENCIES):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EXCHANGE_CURRENCIES, default=[]): vol.Optional(CONF_EXCHANGE_CURRENCIES, default=[]):
vol.All(cv.ensure_list, [cv.string]) vol.All(cv.ensure_list, [cv.string])
}) })
@ -45,6 +48,7 @@ def setup(hass, config):
""" """
api_key = config[DOMAIN].get(CONF_API_KEY) api_key = config[DOMAIN].get(CONF_API_KEY)
api_secret = config[DOMAIN].get(CONF_API_SECRET) api_secret = config[DOMAIN].get(CONF_API_SECRET)
account_currencies = config[DOMAIN].get(CONF_ACCOUNT_CURRENCIES)
exchange_currencies = config[DOMAIN].get(CONF_EXCHANGE_CURRENCIES) exchange_currencies = config[DOMAIN].get(CONF_EXCHANGE_CURRENCIES)
hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData( hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(
@ -53,7 +57,13 @@ def setup(hass, config):
if not hasattr(coinbase_data, 'accounts'): if not hasattr(coinbase_data, 'accounts'):
return False return False
for account in coinbase_data.accounts.data: for account in coinbase_data.accounts.data:
load_platform(hass, 'sensor', DOMAIN, {'account': account}, config) if (account_currencies is None or
account.currency in account_currencies):
load_platform(hass,
'sensor',
DOMAIN,
{'account': account},
config)
for currency in exchange_currencies: for currency in exchange_currencies:
if currency not in coinbase_data.exchange_rates.rates: if currency not in coinbase_data.exchange_rates.rates:
_LOGGER.warning("Currency %s not found", currency) _LOGGER.warning("Currency %s not found", currency)

View File

@ -5,7 +5,8 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.deconz/ https://home-assistant.io/components/cover.deconz/
""" """
from homeassistant.components.deconz.const import ( from homeassistant.components.deconz.const import (
COVER_TYPES, DAMPERS, DOMAIN as DATA_DECONZ, DECONZ_DOMAIN, WINDOW_COVERS) COVER_TYPES, DAMPERS, DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN,
WINDOW_COVERS)
from homeassistant.components.cover import ( from homeassistant.components.cover import (
ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP, ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP,
SUPPORT_SET_POSITION) SUPPORT_SET_POSITION)
@ -29,6 +30,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
Covers are based on same device class as lights in deCONZ. Covers are based on same device class as lights in deCONZ.
""" """
gateway = hass.data[DECONZ_DOMAIN]
@callback @callback
def async_add_cover(lights): def async_add_cover(lights):
"""Add cover from deCONZ.""" """Add cover from deCONZ."""
@ -36,23 +39,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for light in lights: for light in lights:
if light.type in COVER_TYPES: if light.type in COVER_TYPES:
if light.modelid in ZIGBEE_SPEC: if light.modelid in ZIGBEE_SPEC:
entities.append(DeconzCoverZigbeeSpec(light)) entities.append(DeconzCoverZigbeeSpec(light, gateway))
else: else:
entities.append(DeconzCover(light)) entities.append(DeconzCover(light, gateway))
async_add_entities(entities, True) async_add_entities(entities, True)
hass.data[DATA_DECONZ].listeners.append( gateway.listeners.append(
async_dispatcher_connect(hass, 'deconz_new_light', async_add_cover)) async_dispatcher_connect(hass, 'deconz_new_light', async_add_cover))
async_add_cover(hass.data[DATA_DECONZ].api.lights.values()) async_add_cover(gateway.api.lights.values())
class DeconzCover(CoverDevice): class DeconzCover(CoverDevice):
"""Representation of a deCONZ cover.""" """Representation of a deCONZ cover."""
def __init__(self, cover): def __init__(self, cover, gateway):
"""Set up cover and add update callback to get data from websocket.""" """Set up cover and add update callback to get data from websocket."""
self._cover = cover self._cover = cover
self.gateway = gateway
self.unsub_dispatcher = None
self._features = SUPPORT_OPEN self._features = SUPPORT_OPEN
self._features |= SUPPORT_CLOSE self._features |= SUPPORT_CLOSE
self._features |= SUPPORT_STOP self._features |= SUPPORT_STOP
@ -61,11 +67,14 @@ class DeconzCover(CoverDevice):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe to covers events.""" """Subscribe to covers events."""
self._cover.register_async_callback(self.async_update_callback) self._cover.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \ self.gateway.deconz_ids[self.entity_id] = self._cover.deconz_id
self._cover.deconz_id self.unsub_dispatcher = async_dispatcher_connect(
self.hass, DECONZ_REACHABLE, self.async_update_callback)
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Disconnect cover object when removed.""" """Disconnect cover object when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()
self._cover.remove_callback(self.async_update_callback) self._cover.remove_callback(self.async_update_callback)
self._cover = None self._cover = None
@ -112,7 +121,7 @@ class DeconzCover(CoverDevice):
@property @property
def available(self): def available(self):
"""Return True if light is available.""" """Return True if light is available."""
return self._cover.reachable return self.gateway.available and self._cover.reachable
@property @property
def should_poll(self): def should_poll(self):
@ -150,7 +159,7 @@ class DeconzCover(CoverDevice):
self._cover.uniqueid.count(':') != 7): self._cover.uniqueid.count(':') != 7):
return None return None
serial = self._cover.uniqueid.split('-', 1)[0] serial = self._cover.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid bridgeid = self.gateway.api.config.bridgeid
return { return {
'connections': {(CONNECTION_ZIGBEE, serial)}, 'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)},

View File

@ -0,0 +1,92 @@
"""
Support for Fibaro cover - curtains, rollershutters etc.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.fibaro/
"""
import logging
from homeassistant.components.cover import (
CoverDevice, ENTITY_ID_FORMAT, ATTR_POSITION, ATTR_TILT_POSITION)
from homeassistant.components.fibaro import (
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice)
DEPENDENCIES = ['fibaro']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Fibaro covers."""
if discovery_info is None:
return
add_entities(
[FibaroCover(device, hass.data[FIBARO_CONTROLLER]) for
device in hass.data[FIBARO_DEVICES]['cover']], True)
class FibaroCover(FibaroDevice, CoverDevice):
"""Representation a Fibaro Cover."""
def __init__(self, fibaro_device, controller):
"""Initialize the Vera device."""
super().__init__(fibaro_device, controller)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
@staticmethod
def bound(position):
"""Normalize the position."""
if position is None:
return None
position = int(position)
if position <= 5:
return 0
if position >= 95:
return 100
return position
@property
def current_cover_position(self):
"""Return current position of cover. 0 is closed, 100 is open."""
return self.bound(self.level)
@property
def current_cover_tilt_position(self):
"""Return the current tilt position for venetian blinds."""
return self.bound(self.level2)
def set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
self.set_level(kwargs.get(ATTR_POSITION))
def set_cover_tilt_position(self, **kwargs):
"""Move the cover to a specific position."""
self.set_level2(kwargs.get(ATTR_TILT_POSITION))
@property
def is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position is None:
return None
return self.current_cover_position == 0
def open_cover(self, **kwargs):
"""Open the cover."""
self.action("open")
def close_cover(self, **kwargs):
"""Close the cover."""
self.action("close")
def open_cover_tilt(self, **kwargs):
"""Open the cover tilt."""
self.set_level2(100)
def close_cover_tilt(self, **kwargs):
"""Close the cover."""
self.set_level2(0)
def stop_cover(self, **kwargs):
"""Stop the cover."""
self.action("stop")

View File

@ -279,21 +279,19 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
if self._template is not None: if self._template is not None:
payload = self._template.async_render_with_possible_json_value( payload = self._template.async_render_with_possible_json_value(
payload) payload)
if payload.isnumeric(): if payload.isnumeric():
if 0 <= int(payload) <= 100:
percentage_payload = int(payload)
else:
percentage_payload = self.find_percentage_in_range( percentage_payload = self.find_percentage_in_range(
float(payload), COVER_PAYLOAD) float(payload), COVER_PAYLOAD)
if 0 <= percentage_payload <= 100:
self._position = percentage_payload self._position = percentage_payload
self._state = self._position == self._position_closed self._state = percentage_payload == DEFAULT_POSITION_CLOSED
else: else:
_LOGGER.warning( _LOGGER.warning(
"Payload is not integer within range: %s", "Payload is not integer within range: %s",
payload) payload)
return return
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._get_position_topic: if self._get_position_topic:
await mqtt.async_subscribe( await mqtt.async_subscribe(
self.hass, self._get_position_topic, self.hass, self._get_position_topic,
@ -374,7 +372,8 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
# Optimistically assume that cover has changed state. # Optimistically assume that cover has changed state.
self._state = False self._state = False
if self._get_position_topic: if self._get_position_topic:
self._position = self._position_open self._position = self.find_percentage_in_range(
self._position_open, COVER_PAYLOAD)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
async def async_close_cover(self, **kwargs): async def async_close_cover(self, **kwargs):
@ -389,7 +388,8 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
# Optimistically assume that cover has changed state. # Optimistically assume that cover has changed state.
self._state = True self._state = True
if self._get_position_topic: if self._get_position_topic:
self._position = self._position_closed self._position = self.find_percentage_in_range(
self._position_closed, COVER_PAYLOAD)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
async def async_stop_cover(self, **kwargs): async def async_stop_cover(self, **kwargs):
@ -469,6 +469,11 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
offset_position = position - min_range offset_position = position - min_range
position_percentage = round( position_percentage = round(
float(offset_position) / current_range * 100.0) float(offset_position) / current_range * 100.0)
max_percent = 100
min_percent = 0
position_percentage = min(max(position_percentage, min_percent),
max_percent)
if range_type == TILT_PAYLOAD and self._tilt_invert: if range_type == TILT_PAYLOAD and self._tilt_invert:
return 100 - position_percentage return 100 - position_percentage
return position_percentage return position_percentage

View File

@ -9,18 +9,15 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.cover import ( from homeassistant.components.cover import (
CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN) PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, CoverDevice)
from homeassistant.const import ( from homeassistant.const import (
CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_CLOSING, CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_CLOSING,
STATE_OPEN, STATE_OPENING) STATE_OPEN, STATE_OPENING)
import homeassistant.helpers.config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv
REQUIREMENTS = ['pymyq==0.0.15']
REQUIREMENTS = ['pymyq==1.0.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'myq'
MYQ_TO_HASS = { MYQ_TO_HASS = {
'closed': STATE_CLOSED, 'closed': STATE_CLOSED,
'closing': STATE_CLOSING, 'closing': STATE_CLOSING,
@ -28,95 +25,69 @@ MYQ_TO_HASS = {
'opening': STATE_OPENING 'opening': STATE_OPENING
} }
NOTIFICATION_ID = 'myq_notification' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
NOTIFICATION_TITLE = 'MyQ Cover Setup'
COVER_SCHEMA = vol.Schema({
vol.Required(CONF_TYPE): cv.string, vol.Required(CONF_TYPE): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string vol.Required(CONF_PASSWORD): cv.string
}) })
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_platform(
"""Set up the MyQ component.""" hass, config, async_add_entities, discovery_info=None):
from pymyq import MyQAPI as pymyq """Set up the platform."""
from pymyq import login
from pymyq.errors import MyQError, UnsupportedBrandError
username = config.get(CONF_USERNAME) websession = aiohttp_client.async_get_clientsession(hass)
password = config.get(CONF_PASSWORD)
brand = config.get(CONF_TYPE) username = config[CONF_USERNAME]
myq = pymyq(username, password, brand) password = config[CONF_PASSWORD]
brand = config[CONF_TYPE]
try: try:
if not myq.is_supported_brand(): myq = await login(username, password, brand, websession)
raise ValueError("Unsupported type. See documentation") except UnsupportedBrandError:
_LOGGER.error('Unsupported brand: %s', brand)
return
except MyQError as err:
_LOGGER.error('There was an error while logging in: %s', err)
return
if not myq.is_login_valid(): devices = await myq.get_devices()
raise ValueError("Username or Password is incorrect") async_add_entities([MyQDevice(device) for device in devices], True)
add_entities(MyQDevice(myq, door) for door in myq.get_garage_doors())
return True
except (TypeError, KeyError, NameError, ValueError) as ex:
_LOGGER.error("%s", ex)
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
class MyQDevice(CoverDevice): class MyQDevice(CoverDevice):
"""Representation of a MyQ cover.""" """Representation of a MyQ cover."""
def __init__(self, myq, device): def __init__(self, device):
"""Initialize with API object, device id.""" """Initialize with API object, device id."""
self.myq = myq self._device = device
self.device_id = device['deviceid']
self._name = device['name']
self._status = None
@property @property
def device_class(self): def device_class(self):
"""Define this cover as a garage door.""" """Define this cover as a garage door."""
return 'garage' return 'garage'
@property
def should_poll(self):
"""Poll for state."""
return True
@property @property
def name(self): def name(self):
"""Return the name of the garage door if any.""" """Return the name of the garage door if any."""
return self._name if self._name else DEFAULT_NAME return self._device.name
@property @property
def is_closed(self): def is_closed(self):
"""Return true if cover is closed, else False.""" """Return true if cover is closed, else False."""
if self._status in [None, False]: return MYQ_TO_HASS.get(self._device.state) == STATE_CLOSED
return None
return MYQ_TO_HASS.get(self._status) == STATE_CLOSED
@property @property
def is_closing(self): def is_closing(self):
"""Return if the cover is closing or not.""" """Return if the cover is closing or not."""
return MYQ_TO_HASS.get(self._status) == STATE_CLOSING return MYQ_TO_HASS.get(self._device.state) == STATE_CLOSING
@property @property
def is_opening(self): def is_opening(self):
"""Return if the cover is opening or not.""" """Return if the cover is opening or not."""
return MYQ_TO_HASS.get(self._status) == STATE_OPENING return MYQ_TO_HASS.get(self._device.state) == STATE_OPENING
def close_cover(self, **kwargs):
"""Issue close command to cover."""
self.myq.close_device(self.device_id)
def open_cover(self, **kwargs):
"""Issue open command to cover."""
self.myq.open_device(self.device_id)
@property @property
def supported_features(self): def supported_features(self):
@ -126,8 +97,16 @@ class MyQDevice(CoverDevice):
@property @property
def unique_id(self): def unique_id(self):
"""Return a unique, HASS-friendly identifier for this entity.""" """Return a unique, HASS-friendly identifier for this entity."""
return self.device_id return self._device.device_id
def update(self): async def async_close_cover(self, **kwargs):
"""Issue close command to cover."""
await self._device.close()
async def async_open_cover(self, **kwargs):
"""Issue open command to cover."""
await self._device.open()
async def async_update(self):
"""Update status of cover.""" """Update status of cover."""
self._status = self.myq.get_status(self.device_id) await self._device.update()

View File

@ -19,7 +19,7 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.discovery import load_platform
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ['pydaikin==0.6'] REQUIREMENTS = ['pydaikin==0.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -12,7 +12,7 @@
"init": { "init": {
"data": { "data": {
"host": "Host", "host": "Host",
"port": "Port (default value: '80')" "port": "Port"
}, },
"title": "Define deCONZ gateway" "title": "Define deCONZ gateway"
}, },

View File

@ -0,0 +1,33 @@
{
"config": {
"abort": {
"already_configured": "El puente ya esta configurado",
"no_bridges": "No se han descubierto puentes deCONZ",
"one_instance_only": "El componente s\u00f3lo soporta una instancia deCONZ"
},
"error": {
"no_key": "No se pudo obtener una clave API"
},
"step": {
"init": {
"data": {
"host": "Host",
"port": "Puerto"
},
"title": "Definir pasarela deCONZ"
},
"link": {
"description": "Desbloquee su pasarela deCONZ para registrarse con Home Assistant. \n\n 1. Ir a la configuraci\u00f3n del sistema deCONZ \n 2. Presione el bot\u00f3n \"Desbloquear Gateway\"",
"title": "Enlazar con deCONZ"
},
"options": {
"data": {
"allow_clip_sensor": "Permitir importar sensores virtuales",
"allow_deconz_groups": "Permitir la importaci\u00f3n de grupos deCONZ"
},
"title": "Opciones de configuraci\u00f3n adicionales para deCONZ"
}
},
"title": "Pasarela Zigbee deCONZ"
}
}

View File

@ -12,7 +12,7 @@
"init": { "init": {
"data": { "data": {
"host": "Vert", "host": "Vert",
"port": "Port (standardverdi: '80')" "port": "Port"
}, },
"title": "Definer deCONZ-gatewayen" "title": "Definer deCONZ-gatewayen"
}, },

View File

@ -11,11 +11,10 @@ from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.util.json import load_json
# Loading the config flow file will register the flow # Loading the config flow file will register the flow
from .config_flow import configured_hosts from .config_flow import configured_hosts
from .const import CONFIG_FILE, DOMAIN, _LOGGER from .const import DEFAULT_PORT, DOMAIN, _LOGGER
from .gateway import DeconzGateway from .gateway import DeconzGateway
REQUIREMENTS = ['pydeconz==47'] REQUIREMENTS = ['pydeconz==47']
@ -27,7 +26,7 @@ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_API_KEY): cv.string, vol.Optional(CONF_API_KEY): cv.string,
vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=80): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -53,11 +52,7 @@ async def async_setup(hass, config):
""" """
if DOMAIN in config: if DOMAIN in config:
deconz_config = None deconz_config = None
config_file = await hass.async_add_job( if CONF_HOST in config[DOMAIN]:
load_json, hass.config.path(CONFIG_FILE))
if config_file:
deconz_config = config_file
elif CONF_HOST in config[DOMAIN]:
deconz_config = config[DOMAIN] deconz_config = config[DOMAIN]
if deconz_config and not configured_hosts(hass): if deconz_config and not configured_hosts(hass):
hass.async_add_job(hass.config_entries.flow.async_init( hass.async_add_job(hass.config_entries.flow.async_init(

View File

@ -6,11 +6,9 @@ from homeassistant import config_entries
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.util.json import load_json
from .const import ( from .const import (
CONF_ALLOW_DECONZ_GROUPS, CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN) CONF_ALLOW_DECONZ_GROUPS, CONF_ALLOW_CLIP_SENSOR, DEFAULT_PORT, DOMAIN)
CONF_BRIDGEID = 'bridgeid' CONF_BRIDGEID = 'bridgeid'
@ -35,6 +33,10 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
self.deconz_config = {} self.deconz_config = {}
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)
async def async_step_init(self, user_input=None):
"""Handle a deCONZ config flow start. """Handle a deCONZ config flow start.
Only allows one instance to be set up. Only allows one instance to be set up.
@ -51,6 +53,8 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
if bridge[CONF_HOST] == user_input[CONF_HOST]: if bridge[CONF_HOST] == user_input[CONF_HOST]:
self.deconz_config = bridge self.deconz_config = bridge
return await self.async_step_link() return await self.async_step_link()
self.deconz_config = user_input
return await self.async_step_link()
session = aiohttp_client.async_get_clientsession(self.hass) session = aiohttp_client.async_get_clientsession(self.hass)
self.bridges = await async_discovery(session) self.bridges = await async_discovery(session)
@ -58,19 +62,24 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
if len(self.bridges) == 1: if len(self.bridges) == 1:
self.deconz_config = self.bridges[0] self.deconz_config = self.bridges[0]
return await self.async_step_link() return await self.async_step_link()
if len(self.bridges) > 1: if len(self.bridges) > 1:
hosts = [] hosts = []
for bridge in self.bridges: for bridge in self.bridges:
hosts.append(bridge[CONF_HOST]) hosts.append(bridge[CONF_HOST])
return self.async_show_form( return self.async_show_form(
step_id='user', step_id='init',
data_schema=vol.Schema({ data_schema=vol.Schema({
vol.Required(CONF_HOST): vol.In(hosts) vol.Required(CONF_HOST): vol.In(hosts)
}) })
) )
return self.async_abort( return self.async_show_form(
reason='no_bridges' step_id='user',
data_schema=vol.Schema({
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}),
) )
async def async_step_link(self, user_input=None): async def async_step_link(self, user_input=None):
@ -135,13 +144,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT) deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
deconz_config[CONF_BRIDGEID] = discovery_info.get('serial') deconz_config[CONF_BRIDGEID] = discovery_info.get('serial')
config_file = await self.hass.async_add_job(
load_json, self.hass.config.path(CONFIG_FILE))
if config_file and \
config_file[CONF_HOST] == deconz_config[CONF_HOST] and \
CONF_API_KEY in config_file:
deconz_config[CONF_API_KEY] = config_file[CONF_API_KEY]
return await self.async_step_import(deconz_config) return await self.async_step_import(deconz_config)
async def async_step_import(self, import_config): async def async_step_import(self, import_config):

View File

@ -4,11 +4,8 @@ import logging
_LOGGER = logging.getLogger('homeassistant.components.deconz') _LOGGER = logging.getLogger('homeassistant.components.deconz')
DOMAIN = 'deconz' DOMAIN = 'deconz'
CONFIG_FILE = 'deconz.conf'
DATA_DECONZ_EVENT = 'deconz_events' DEFAULT_PORT = 80
DATA_DECONZ_ID = 'deconz_entities'
DATA_DECONZ_UNSUB = 'deconz_dispatchers'
DECONZ_DOMAIN = 'deconz'
CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor'
CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
@ -16,6 +13,8 @@ CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
SUPPORTED_PLATFORMS = ['binary_sensor', 'cover', SUPPORTED_PLATFORMS = ['binary_sensor', 'cover',
'light', 'scene', 'sensor', 'switch'] 'light', 'scene', 'sensor', 'switch']
DECONZ_REACHABLE = 'deconz_reachable'
ATTR_DARK = 'dark' ATTR_DARK = 'dark'
ATTR_ON = 'on' ATTR_ON = 'on'

View File

@ -8,7 +8,7 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.util import slugify from homeassistant.util import slugify
from .const import ( from .const import (
_LOGGER, CONF_ALLOW_CLIP_SENSOR, SUPPORTED_PLATFORMS) _LOGGER, DECONZ_REACHABLE, CONF_ALLOW_CLIP_SENSOR, SUPPORTED_PLATFORMS)
class DeconzGateway: class DeconzGateway:
@ -18,6 +18,7 @@ class DeconzGateway:
"""Initialize the system.""" """Initialize the system."""
self.hass = hass self.hass = hass
self.config_entry = config_entry self.config_entry = config_entry
self.available = True
self.api = None self.api = None
self._cancel_retry_setup = None self._cancel_retry_setup = None
@ -30,7 +31,8 @@ class DeconzGateway:
hass = self.hass hass = self.hass
self.api = await get_gateway( self.api = await get_gateway(
hass, self.config_entry.data, self.async_add_device_callback hass, self.config_entry.data, self.async_add_device_callback,
self.async_connection_status_callback
) )
if self.api is False: if self.api is False:
@ -65,6 +67,13 @@ class DeconzGateway:
return True return True
@callback
def async_connection_status_callback(self, available):
"""Handle signals of gateway connection status."""
self.available = available
async_dispatcher_send(
self.hass, DECONZ_REACHABLE, {'state': True, 'attr': 'reachable'})
@callback @callback
def async_add_device_callback(self, device_type, device): def async_add_device_callback(self, device_type, device):
"""Handle event of new device creation in deCONZ.""" """Handle event of new device creation in deCONZ."""
@ -122,13 +131,15 @@ class DeconzGateway:
return True return True
async def get_gateway(hass, config, async_add_device_callback): async def get_gateway(hass, config, async_add_device_callback,
async_connection_status_callback):
"""Create a gateway object and verify configuration.""" """Create a gateway object and verify configuration."""
from pydeconz import DeconzSession from pydeconz import DeconzSession
session = aiohttp_client.async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, session, **config, deconz = DeconzSession(hass.loop, session, **config,
async_add_device=async_add_device_callback) async_add_device=async_add_device_callback,
connection_status=async_connection_status_callback)
result = await deconz.async_load_parameters() result = await deconz.async_load_parameters()
if result: if result:

View File

@ -6,7 +6,7 @@
"title": "Define deCONZ gateway", "title": "Define deCONZ gateway",
"data": { "data": {
"host": "Host", "host": "Host",
"port": "Port (default value: '80')" "port": "Port"
} }
}, },
"link": { "link": {

View File

@ -182,6 +182,9 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
setup = await hass.async_add_job( setup = await hass.async_add_job(
platform.setup_scanner, hass, p_config, tracker.see, platform.setup_scanner, hass, p_config, tracker.see,
disc_info) disc_info)
elif hasattr(platform, 'async_setup_entry'):
setup = await platform.async_setup_entry(
hass, p_config, tracker.async_see)
else: else:
raise HomeAssistantError("Invalid device_tracker platform.") raise HomeAssistantError("Invalid device_tracker platform.")
@ -197,6 +200,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error setting up platform %s", p_type) _LOGGER.exception("Error setting up platform %s", p_type)
hass.data[DOMAIN] = async_setup_platform
setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config
in config_per_platform(config, DOMAIN)] in config_per_platform(config, DOMAIN)]
if setup_tasks: if setup_tasks:
@ -230,6 +235,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
return True return True
async def async_setup_entry(hass, entry):
"""Set up an entry."""
await hass.data[DOMAIN](entry.domain, entry)
return True
class DeviceTracker: class DeviceTracker:
"""Representation of a device tracker.""" """Representation of a device tracker."""
@ -373,6 +384,7 @@ class DeviceTracker:
for device in self.devices.values(): for device in self.devices.values():
if (device.track and device.last_update_home) and \ if (device.track and device.last_update_home) and \
device.stale(now): device.stale(now):
device.mark_stale()
self.hass.async_create_task(device.async_update_ha_state(True)) self.hass.async_create_task(device.async_update_ha_state(True))
async def async_setup_tracked_device(self): async def async_setup_tracked_device(self):
@ -528,9 +540,15 @@ class Device(Entity):
Async friendly. Async friendly.
""" """
return self.last_seen and \ return self.last_seen is None or \
(now or dt_util.utcnow()) - self.last_seen > self.consider_home (now or dt_util.utcnow()) - self.last_seen > self.consider_home
def mark_stale(self):
"""Mark the device state as stale."""
self._state = STATE_NOT_HOME
self.gps = None
self.last_update_home = False
async def async_update(self): async def async_update(self):
"""Update state of entity. """Update state of entity.
@ -550,9 +568,7 @@ class Device(Entity):
else: else:
self._state = zone_state.name self._state = zone_state.name
elif self.stale(): elif self.stale():
self._state = STATE_NOT_HOME self.mark_stale()
self.gps = None
self.last_update_home = False
else: else:
self._state = STATE_HOME self._state = STATE_HOME
self.last_update_home = True self.last_update_home = True
@ -563,6 +579,7 @@ class Device(Entity):
if not state: if not state:
return return
self._state = state.state self._state = state.state
self.last_update_home = (state.state == STATE_HOME)
for attr, var in ( for attr, var in (
(ATTR_SOURCE_TYPE, 'source_type'), (ATTR_SOURCE_TYPE, 'source_type'),

View File

@ -6,43 +6,17 @@ https://home-assistant.io/components/device_tracker.asuswrt/
""" """
import logging import logging
import voluptuous as vol from homeassistant.components.asuswrt import DATA_ASUSWRT
from homeassistant.components.device_tracker import DeviceScanner
import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['asuswrt']
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_MODE,
CONF_PROTOCOL)
REQUIREMENTS = ['aioasuswrt==1.1.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_PUB_KEY = 'pub_key'
CONF_SSH_KEY = 'ssh_key'
CONF_REQUIRE_IP = 'require_ip'
DEFAULT_SSH_PORT = 22
SECRET_GROUP = 'Password or SSH Key'
PLATFORM_SCHEMA = vol.All(
cv.has_at_least_one_key(CONF_PASSWORD, CONF_PUB_KEY, CONF_SSH_KEY),
PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']),
vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']),
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean,
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile
}))
async def async_get_scanner(hass, config): async def async_get_scanner(hass, config):
"""Validate the configuration and return an ASUS-WRT scanner.""" """Validate the configuration and return an ASUS-WRT scanner."""
scanner = AsusWrtDeviceScanner(config[DOMAIN]) scanner = AsusWrtDeviceScanner(hass.data[DATA_ASUSWRT])
await scanner.async_connect() await scanner.async_connect()
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
@ -51,19 +25,11 @@ class AsusWrtDeviceScanner(DeviceScanner):
"""This class queries a router running ASUSWRT firmware.""" """This class queries a router running ASUSWRT firmware."""
# Eighth attribute needed for mode (AP mode vs router mode) # Eighth attribute needed for mode (AP mode vs router mode)
def __init__(self, config): def __init__(self, api):
"""Initialize the scanner.""" """Initialize the scanner."""
from aioasuswrt.asuswrt import AsusWrt
self.last_results = {} self.last_results = {}
self.success_init = False self.success_init = False
self.connection = AsusWrt(config[CONF_HOST], config[CONF_PORT], self.connection = api
config[CONF_PROTOCOL] == 'telnet',
config[CONF_USERNAME],
config.get(CONF_PASSWORD, ''),
config.get('ssh_key',
config.get('pub_key', '')),
config[CONF_MODE], config[CONF_REQUIRE_IP])
async def async_connect(self): async def async_connect(self):
"""Initialize connection to the router.""" """Initialize connection to the router."""

View File

@ -4,129 +4,26 @@ Support for the Geofency platform.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.geofency/ https://home-assistant.io/components/device_tracker.geofency/
""" """
from functools import partial
import logging import logging
import voluptuous as vol from homeassistant.components.geofency import TRACKER_UPDATE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
ATTR_LATITUDE, ATTR_LONGITUDE, HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http'] DEPENDENCIES = ['geofency']
ATTR_CURRENT_LATITUDE = 'currentLatitude'
ATTR_CURRENT_LONGITUDE = 'currentLongitude'
BEACON_DEV_PREFIX = 'beacon'
CONF_MOBILE_BEACONS = 'mobile_beacons'
LOCATION_ENTRY = '1'
LOCATION_EXIT = '0'
URL = '/api/geofency'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MOBILE_BEACONS): vol.All(
cv.ensure_list, [cv.string]),
})
def setup_scanner(hass, config, see, discovery_info=None): async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an endpoint for the Geofency application.""" """Set up the Geofency device tracker."""
mobile_beacons = config.get(CONF_MOBILE_BEACONS) or [] async def _set_location(device, gps, location_name, attributes):
hass.http.register_view(GeofencyView(see, mobile_beacons))
return True
class GeofencyView(HomeAssistantView):
"""View to handle Geofency requests."""
url = URL
name = 'api:geofency'
def __init__(self, see, mobile_beacons):
"""Initialize Geofency url endpoints."""
self.see = see
self.mobile_beacons = [slugify(beacon) for beacon in mobile_beacons]
async def post(self, request):
"""Handle Geofency requests."""
data = await request.post()
hass = request.app['hass']
data = self._validate_data(data)
if not data:
return ("Invalid data", HTTP_UNPROCESSABLE_ENTITY)
if self._is_mobile_beacon(data):
return await self._set_location(hass, data, None)
if data['entry'] == LOCATION_ENTRY:
location_name = data['name']
else:
location_name = STATE_NOT_HOME
if ATTR_CURRENT_LATITUDE in data:
data[ATTR_LATITUDE] = data[ATTR_CURRENT_LATITUDE]
data[ATTR_LONGITUDE] = data[ATTR_CURRENT_LONGITUDE]
return await self._set_location(hass, data, location_name)
@staticmethod
def _validate_data(data):
"""Validate POST payload."""
data = data.copy()
required_attributes = ['address', 'device', 'entry',
'latitude', 'longitude', 'name']
valid = True
for attribute in required_attributes:
if attribute not in data:
valid = False
_LOGGER.error("'%s' not specified in message", attribute)
if not valid:
return False
data['address'] = data['address'].replace('\n', ' ')
data['device'] = slugify(data['device'])
data['name'] = slugify(data['name'])
gps_attributes = [ATTR_LATITUDE, ATTR_LONGITUDE,
ATTR_CURRENT_LATITUDE, ATTR_CURRENT_LONGITUDE]
for attribute in gps_attributes:
if attribute in data:
data[attribute] = float(data[attribute])
return data
def _is_mobile_beacon(self, data):
"""Check if we have a mobile beacon."""
return 'beaconUUID' in data and data['name'] in self.mobile_beacons
@staticmethod
def _device_name(data):
"""Return name of device tracker."""
if 'beaconUUID' in data:
return "{}_{}".format(BEACON_DEV_PREFIX, data['name'])
return data['device']
async def _set_location(self, hass, data, location_name):
"""Fire HA event to set location.""" """Fire HA event to set location."""
device = self._device_name(data) await async_see(
dev_id=device,
await hass.async_add_job( gps=gps,
partial(self.see, dev_id=device,
gps=(data[ATTR_LATITUDE], data[ATTR_LONGITUDE]),
location_name=location_name, location_name=location_name,
attributes=data)) attributes=attributes
)
return "Setting location for {}".format(device) async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location)
return True

View File

@ -19,7 +19,7 @@ from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify, dt as dt_util from homeassistant.util import slugify, dt as dt_util
REQUIREMENTS = ['locationsharinglib==3.0.7'] REQUIREMENTS = ['locationsharinglib==3.0.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,92 @@
"""
Support for Google Home bluetooth tacker.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.googlehome/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
REQUIREMENTS = ['ghlocalapi==0.1.0']
_LOGGER = logging.getLogger(__name__)
CONF_RSSI_THRESHOLD = 'rssi_threshold'
PLATFORM_SCHEMA = vol.All(
PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_RSSI_THRESHOLD, default=-70): vol.Coerce(int),
}))
async def async_get_scanner(hass, config):
"""Validate the configuration and return an Google Home scanner."""
scanner = GoogleHomeDeviceScanner(hass, config[DOMAIN])
await scanner.async_connect()
return scanner if scanner.success_init else None
class GoogleHomeDeviceScanner(DeviceScanner):
"""This class queries a Google Home unit."""
def __init__(self, hass, config):
"""Initialize the scanner."""
from ghlocalapi.device_info import DeviceInfo
from ghlocalapi.bluetooth import Bluetooth
self.last_results = {}
self.success_init = False
self._host = config[CONF_HOST]
self.rssi_threshold = config[CONF_RSSI_THRESHOLD]
session = async_get_clientsession(hass)
self.deviceinfo = DeviceInfo(hass.loop, session, self._host)
self.scanner = Bluetooth(hass.loop, session, self._host)
async def async_connect(self):
"""Initialize connection to Google Home."""
await self.deviceinfo.get_device_info()
data = self.deviceinfo.device_info
self.success_init = data is not None
async def async_scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
await self.async_update_info()
return list(self.last_results.keys())
async def async_get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
if device not in self.last_results:
return None
return '{}_{}'.format(self._host,
self.last_results[device]['btle_mac_address'])
async def get_extra_attributes(self, device):
"""Return the extra attributes of the device."""
return self.last_results[device]
async def async_update_info(self):
"""Ensure the information from Google Home is up to date."""
_LOGGER.debug('Checking Devices...')
await self.scanner.scan_for_devices()
await self.scanner.get_scan_result()
ghname = self.deviceinfo.device_info['name']
devices = {}
for device in self.scanner.devices:
if device['rssi'] > self.rssi_threshold:
uuid = '{}_{}'.format(self._host, device['mac_address'])
devices[uuid] = {}
devices[uuid]['rssi'] = device['rssi']
devices[uuid]['btle_mac_address'] = device['mac_address']
devices[uuid]['ghname'] = ghname
devices[uuid]['source_type'] = 'bluetooth'
self.last_results = devices

View File

@ -7,6 +7,7 @@ https://home-assistant.io/components/device_tracker.luci/
import json import json
import logging import logging
import re import re
from collections import namedtuple
import requests import requests
import voluptuous as vol import voluptuous as vol
@ -43,14 +44,17 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
Device = namedtuple('Device', ['mac', 'ip', 'flags', 'device', 'host'])
class LuciDeviceScanner(DeviceScanner): class LuciDeviceScanner(DeviceScanner):
"""This class queries a wireless router running OpenWrt firmware.""" """This class queries a wireless router running OpenWrt firmware."""
def __init__(self, config): def __init__(self, config):
"""Initialize the scanner.""" """Initialize the scanner."""
host = config[CONF_HOST] self.host = config[CONF_HOST]
protocol = 'http' if not config[CONF_SSL] else 'https' protocol = 'http' if not config[CONF_SSL] else 'https'
self.origin = '{}://{}'.format(protocol, host) self.origin = '{}://{}'.format(protocol, self.host)
self.username = config[CONF_USERNAME] self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD] self.password = config[CONF_PASSWORD]
@ -68,7 +72,7 @@ class LuciDeviceScanner(DeviceScanner):
def scan_devices(self): def scan_devices(self):
"""Scan for new devices and return a list with found device IDs.""" """Scan for new devices and return a list with found device IDs."""
self._update_info() self._update_info()
return self.last_results return [device.mac for device in self.last_results]
def get_device_name(self, device): def get_device_name(self, device):
"""Return the name of the given device or None if we don't know.""" """Return the name of the given device or None if we don't know."""
@ -88,6 +92,18 @@ class LuciDeviceScanner(DeviceScanner):
return return
return self.mac2name.get(device.upper(), None) return self.mac2name.get(device.upper(), None)
def get_extra_attributes(self, device):
"""Return the IP of the given device."""
filter_att = next((
{
'ip': result.ip,
'flags': result.flags,
'device': result.device,
'host': result.host
} for result in self.last_results
if result.mac == device), None)
return filter_att
def _update_info(self): def _update_info(self):
"""Ensure the information from the Luci router is up to date. """Ensure the information from the Luci router is up to date.
@ -114,7 +130,11 @@ class LuciDeviceScanner(DeviceScanner):
# Check if the Flags for each device contain # Check if the Flags for each device contain
# NUD_REACHABLE and if so, add it to last_results # NUD_REACHABLE and if so, add it to last_results
if int(device_entry['Flags'], 16) & 0x2: if int(device_entry['Flags'], 16) & 0x2:
self.last_results.append(device_entry['HW address']) self.last_results.append(Device(device_entry['HW address'],
device_entry['IP address'],
device_entry['Flags'],
device_entry['Device'],
self.host))
return True return True

View File

@ -6,25 +6,30 @@ https://home-assistant.io/components/device_tracker.mikrotik/
""" """
import logging import logging
import ssl
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL, CONF_METHOD)
REQUIREMENTS = ['librouteros==2.1.1'] REQUIREMENTS = ['librouteros==2.1.1']
MTK_DEFAULT_API_PORT = '8728'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MTK_DEFAULT_API_PORT = '8728'
MTK_DEFAULT_API_SSL_PORT = '8729'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PORT, default=MTK_DEFAULT_API_PORT): cv.port vol.Optional(CONF_METHOD): cv.string,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_SSL, default=False): cv.boolean
}) })
@ -42,9 +47,17 @@ class MikrotikScanner(DeviceScanner):
self.last_results = {} self.last_results = {}
self.host = config[CONF_HOST] self.host = config[CONF_HOST]
self.ssl = config[CONF_SSL]
try:
self.port = config[CONF_PORT] self.port = config[CONF_PORT]
except KeyError:
if self.ssl:
self.port = MTK_DEFAULT_API_SSL_PORT
else:
self.port = MTK_DEFAULT_API_PORT
self.username = config[CONF_USERNAME] self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD] self.password = config[CONF_PASSWORD]
self.method = config.get(CONF_METHOD)
self.connected = False self.connected = False
self.success_init = False self.success_init = False
@ -53,27 +66,29 @@ class MikrotikScanner(DeviceScanner):
self.success_init = self.connect_to_device() self.success_init = self.connect_to_device()
if self.success_init: if self.success_init:
_LOGGER.info( _LOGGER.info("Start polling Mikrotik (%s) router...", self.host)
"Start polling Mikrotik (%s) router...",
self.host
)
self._update_info() self._update_info()
else: else:
_LOGGER.error( _LOGGER.error("Connection to Mikrotik (%s) failed", self.host)
"Connection to Mikrotik (%s) failed",
self.host
)
def connect_to_device(self): def connect_to_device(self):
"""Connect to Mikrotik method.""" """Connect to Mikrotik method."""
import librouteros import librouteros
try: try:
kwargs = {
'port': self.port,
'encoding': 'utf-8'
}
if self.ssl:
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
kwargs['ssl_wrapper'] = ssl_context.wrap_socket
self.client = librouteros.connect( self.client = librouteros.connect(
self.host, self.host,
self.username, self.username,
self.password, self.password,
port=int(self.port), **kwargs
encoding='utf-8'
) )
try: try:
@ -86,16 +101,15 @@ class MikrotikScanner(DeviceScanner):
raise raise
if routerboard_info: if routerboard_info:
_LOGGER.info("Connected to Mikrotik %s with IP %s", _LOGGER.info(
routerboard_info[0].get('model', 'Router'), "Connected to Mikrotik %s with IP %s",
self.host) routerboard_info[0].get('model', 'Router'), self.host)
self.connected = True self.connected = True
try: try:
self.capsman_exist = self.client( self.capsman_exist = self.client(
cmd='/caps-man/interface/getall' cmd='/caps-man/interface/getall')
)
except (librouteros.exceptions.TrapError, except (librouteros.exceptions.TrapError,
librouteros.exceptions.MultiTrapError, librouteros.exceptions.MultiTrapError,
librouteros.exceptions.ConnectionError): librouteros.exceptions.ConnectionError):
@ -103,27 +117,27 @@ class MikrotikScanner(DeviceScanner):
if not self.capsman_exist: if not self.capsman_exist:
_LOGGER.info( _LOGGER.info(
'Mikrotik %s: Not a CAPSman controller. Trying ' "Mikrotik %s: Not a CAPSman controller. Trying "
'local interfaces ', "local interfaces", self.host)
self.host
)
try: try:
self.wireless_exist = self.client( self.wireless_exist = self.client(
cmd='/interface/wireless/getall' cmd='/interface/wireless/getall')
)
except (librouteros.exceptions.TrapError, except (librouteros.exceptions.TrapError,
librouteros.exceptions.MultiTrapError, librouteros.exceptions.MultiTrapError,
librouteros.exceptions.ConnectionError): librouteros.exceptions.ConnectionError):
self.wireless_exist = False self.wireless_exist = False
if not self.wireless_exist: if not self.wireless_exist or self.method == 'ip':
_LOGGER.info( _LOGGER.info(
'Mikrotik %s: Wireless adapters not found. Try to ' "Mikrotik %s: Wireless adapters not found. Try to "
'use DHCP lease table as presence tracker source. ' "use DHCP lease table as presence tracker source. "
'Please decrease lease time as much as possible.', "Please decrease lease time as much as possible",
self.host self.host)
) if self.method:
_LOGGER.info(
"Mikrotik %s: Manually selected polling method %s",
self.host, self.method)
except (librouteros.exceptions.TrapError, except (librouteros.exceptions.TrapError,
librouteros.exceptions.MultiTrapError, librouteros.exceptions.MultiTrapError,
@ -143,6 +157,9 @@ class MikrotikScanner(DeviceScanner):
def _update_info(self): def _update_info(self):
"""Retrieve latest information from the Mikrotik box.""" """Retrieve latest information from the Mikrotik box."""
if self.method:
devices_tracker = self.method
else:
if self.capsman_exist: if self.capsman_exist:
devices_tracker = 'capsman' devices_tracker = 'capsman'
elif self.wireless_exist: elif self.wireless_exist:
@ -152,19 +169,15 @@ class MikrotikScanner(DeviceScanner):
_LOGGER.info( _LOGGER.info(
"Loading %s devices from Mikrotik (%s) ...", "Loading %s devices from Mikrotik (%s) ...",
devices_tracker, devices_tracker, self.host)
self.host
)
device_names = self.client(cmd='/ip/dhcp-server/lease/getall') device_names = self.client(cmd='/ip/dhcp-server/lease/getall')
if devices_tracker == 'capsman': if devices_tracker == 'capsman':
devices = self.client( devices = self.client(
cmd='/caps-man/registration-table/getall' cmd='/caps-man/registration-table/getall')
)
elif devices_tracker == 'wireless': elif devices_tracker == 'wireless':
devices = self.client( devices = self.client(
cmd='/interface/wireless/registration-table/getall' cmd='/interface/wireless/registration-table/getall')
)
else: else:
devices = device_names devices = device_names
@ -172,21 +185,17 @@ class MikrotikScanner(DeviceScanner):
return False return False
mac_names = {device.get('mac-address'): device.get('host-name') mac_names = {device.get('mac-address'): device.get('host-name')
for device in device_names for device in device_names if device.get('mac-address')}
if device.get('mac-address')}
if self.wireless_exist or self.capsman_exist: if devices_tracker in ('wireless', 'capsman'):
self.last_results = { self.last_results = {
device.get('mac-address'): device.get('mac-address'):
mac_names.get(device.get('mac-address')) mac_names.get(device.get('mac-address'))
for device in devices for device in devices}
}
else: else:
self.last_results = { self.last_results = {
device.get('mac-address'): device.get('mac-address'):
mac_names.get(device.get('mac-address')) mac_names.get(device.get('mac-address'))
for device in device_names for device in device_names if device.get('active-address')}
if device.get('active-address')
}
return True return True

View File

@ -19,11 +19,16 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
return False return False
for device in new_devices: for device in new_devices:
gateway_id = id(device.gateway)
dev_id = ( dev_id = (
id(device.gateway), device.node_id, device.child_id, gateway_id, device.node_id, device.child_id,
device.value_type) device.value_type)
async_dispatcher_connect( async_dispatcher_connect(
hass, mysensors.const.SIGNAL_CALLBACK.format(*dev_id), hass, mysensors.const.CHILD_CALLBACK.format(*dev_id),
device.async_update_callback)
async_dispatcher_connect(
hass,
mysensors.const.NODE_CALLBACK.format(gateway_id, device.node_id),
device.async_update_callback) device.async_update_callback)
return True return True

View File

@ -7,55 +7,29 @@ https://home-assistant.io/components/device_tracker.owntracks/
import base64 import base64
import json import json
import logging import logging
from collections import defaultdict
import voluptuous as vol
from homeassistant.components import mqtt
import homeassistant.helpers.config_validation as cv
from homeassistant.components import zone as zone_comp from homeassistant.components import zone as zone_comp
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, ATTR_SOURCE_TYPE, SOURCE_TYPE_BLUETOOTH_LE, ATTR_SOURCE_TYPE, SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS
SOURCE_TYPE_GPS
) )
from homeassistant.components.owntracks import DOMAIN as OT_DOMAIN
from homeassistant.const import STATE_HOME from homeassistant.const import STATE_HOME
from homeassistant.core import callback
from homeassistant.util import slugify, decorator from homeassistant.util import slugify, decorator
REQUIREMENTS = ['libnacl==1.6.1']
DEPENDENCIES = ['owntracks']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
HANDLERS = decorator.Registry() HANDLERS = decorator.Registry()
BEACON_DEV_ID = 'beacon'
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' async def async_setup_entry(hass, entry, async_see):
CONF_SECRET = 'secret' """Set up OwnTracks based off an entry."""
CONF_WAYPOINT_IMPORT = 'waypoints' hass.data[OT_DOMAIN]['context'].async_see = async_see
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist' hass.helpers.dispatcher.async_dispatcher_connect(
CONF_MQTT_TOPIC = 'mqtt_topic' OT_DOMAIN, async_handle_message)
CONF_REGION_MAPPING = 'region_mapping' return True
CONF_EVENTS_ONLY = 'events_only'
DEPENDENCIES = ['mqtt']
DEFAULT_OWNTRACKS_TOPIC = 'owntracks/#'
REGION_MAPPING = {}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
vol.Optional(CONF_WAYPOINT_IMPORT, default=True): cv.boolean,
vol.Optional(CONF_EVENTS_ONLY, default=False): cv.boolean,
vol.Optional(CONF_MQTT_TOPIC, default=DEFAULT_OWNTRACKS_TOPIC):
mqtt.valid_subscribe_topic,
vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(
cv.ensure_list, [cv.string]),
vol.Optional(CONF_SECRET): vol.Any(
vol.Schema({vol.Optional(cv.string): cv.string}),
cv.string),
vol.Optional(CONF_REGION_MAPPING, default=REGION_MAPPING): dict
})
def get_cipher(): def get_cipher():
@ -72,29 +46,6 @@ def get_cipher():
return (KEYLEN, decrypt) return (KEYLEN, decrypt)
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an OwnTracks tracker."""
context = context_from_config(async_see, config)
async def async_handle_mqtt_message(topic, payload, qos):
"""Handle incoming OwnTracks message."""
try:
message = json.loads(payload)
except ValueError:
# If invalid JSON
_LOGGER.error("Unable to parse payload as JSON: %s", payload)
return
message['topic'] = topic
await async_handle_message(hass, context, message)
await mqtt.async_subscribe(
hass, context.mqtt_topic, async_handle_mqtt_message, 1)
return True
def _parse_topic(topic, subscribe_topic): def _parse_topic(topic, subscribe_topic):
"""Parse an MQTT topic {sub_topic}/user/dev, return (user, dev) tuple. """Parse an MQTT topic {sub_topic}/user/dev, return (user, dev) tuple.
@ -202,93 +153,6 @@ def _decrypt_payload(secret, topic, ciphertext):
return None return None
def context_from_config(async_see, config):
"""Create an async context from Home Assistant config."""
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
secret = config.get(CONF_SECRET)
region_mapping = config.get(CONF_REGION_MAPPING)
events_only = config.get(CONF_EVENTS_ONLY)
mqtt_topic = config.get(CONF_MQTT_TOPIC)
return OwnTracksContext(async_see, secret, max_gps_accuracy,
waypoint_import, waypoint_whitelist,
region_mapping, events_only, mqtt_topic)
class OwnTracksContext:
"""Hold the current OwnTracks context."""
def __init__(self, async_see, secret, max_gps_accuracy, import_waypoints,
waypoint_whitelist, region_mapping, events_only, mqtt_topic):
"""Initialize an OwnTracks context."""
self.async_see = async_see
self.secret = secret
self.max_gps_accuracy = max_gps_accuracy
self.mobile_beacons_active = defaultdict(set)
self.regions_entered = defaultdict(list)
self.import_waypoints = import_waypoints
self.waypoint_whitelist = waypoint_whitelist
self.region_mapping = region_mapping
self.events_only = events_only
self.mqtt_topic = mqtt_topic
@callback
def async_valid_accuracy(self, message):
"""Check if we should ignore this message."""
acc = message.get('acc')
if acc is None:
return False
try:
acc = float(acc)
except ValueError:
return False
if acc == 0:
_LOGGER.warning(
"Ignoring %s update because GPS accuracy is zero: %s",
message['_type'], message)
return False
if self.max_gps_accuracy is not None and \
acc > self.max_gps_accuracy:
_LOGGER.info("Ignoring %s update because expected GPS "
"accuracy %s is not met: %s",
message['_type'], self.max_gps_accuracy,
message)
return False
return True
async def async_see_beacons(self, hass, dev_id, kwargs_param):
"""Set active beacons to the current location."""
kwargs = kwargs_param.copy()
# Mobile beacons should always be set to the location of the
# tracking device. I get the device state and make the necessary
# changes to kwargs.
device_tracker_state = hass.states.get(
"device_tracker.{}".format(dev_id))
if device_tracker_state is not None:
acc = device_tracker_state.attributes.get("gps_accuracy")
lat = device_tracker_state.attributes.get("latitude")
lon = device_tracker_state.attributes.get("longitude")
kwargs['gps_accuracy'] = acc
kwargs['gps'] = (lat, lon)
# the battery state applies to the tracking device, not the beacon
# kwargs location is the beacon's configured lat/lon
kwargs.pop('battery', None)
for beacon in self.mobile_beacons_active[dev_id]:
kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon)
kwargs['host_name'] = beacon
await self.async_see(**kwargs)
@HANDLERS.register('location') @HANDLERS.register('location')
async def async_handle_location_message(hass, context, message): async def async_handle_location_message(hass, context, message):
"""Handle a location message.""" """Handle a location message."""
@ -485,6 +349,8 @@ async def async_handle_message(hass, context, message):
"""Handle an OwnTracks message.""" """Handle an OwnTracks message."""
msgtype = message.get('_type') msgtype = message.get('_type')
_LOGGER.debug("Received %s", message)
handler = HANDLERS.get(msgtype, async_handle_unsupported_msg) handler = HANDLERS.get(msgtype, async_handle_unsupported_msg)
await handler(hass, context, message) await handler(hass, context, message)

View File

@ -1,55 +0,0 @@
"""
Device tracker platform that adds support for OwnTracks over HTTP.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.owntracks_http/
"""
import re
from aiohttp.web_exceptions import HTTPInternalServerError
from homeassistant.components.http import HomeAssistantView
# pylint: disable=unused-import
from .owntracks import ( # NOQA
REQUIREMENTS, PLATFORM_SCHEMA, context_from_config, async_handle_message)
DEPENDENCIES = ['http']
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an OwnTracks tracker."""
context = context_from_config(async_see, config)
hass.http.register_view(OwnTracksView(context))
return True
class OwnTracksView(HomeAssistantView):
"""View to handle OwnTracks HTTP requests."""
url = '/api/owntracks/{user}/{device}'
name = 'api:owntracks'
def __init__(self, context):
"""Initialize OwnTracks URL endpoints."""
self.context = context
async def post(self, request, user, device):
"""Handle an OwnTracks message."""
hass = request.app['hass']
subscription = self.context.mqtt_topic
topic = re.sub('/#$', '', subscription)
message = await request.json()
message['topic'] = '{}/{}/{}'.format(topic, user, device)
try:
await async_handle_message(hass, self.context, message)
return self.json([])
except ValueError:
raise HTTPInternalServerError

View File

@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})') _MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string vol.Optional(CONF_HOST): cv.string
}) })

View File

@ -18,7 +18,7 @@ from homeassistant.util import slugify
from homeassistant.util.json import load_json, save_json from homeassistant.util.json import load_json, save_json
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pytile==2.0.2'] REQUIREMENTS = ['pytile==2.0.5']
CLIENT_UUID_CONFIG_FILE = '.tile.conf' CLIENT_UUID_CONFIG_FILE = '.tile.conf'
DEVICE_TYPES = ['PHONE', 'TILE'] DEVICE_TYPES = ['PHONE', 'TILE']

View File

@ -0,0 +1,90 @@
"""
Support for Traccar device tracking.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.traccar/
"""
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL,
CONF_PASSWORD, CONF_USERNAME)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util import slugify
REQUIREMENTS = ['pytraccar==0.1.2']
_LOGGER = logging.getLogger(__name__)
ATTR_ADDRESS = 'address'
ATTR_CATEGORY = 'category'
ATTR_GEOFENCE = 'geofence'
ATTR_TRACKER = 'tracker'
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=8082): cv.port,
vol.Optional(CONF_SSL, default=False): cv.boolean,
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
})
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Validate the configuration and return a Traccar scanner."""
from pytraccar.api import API
session = async_get_clientsession(hass, config[CONF_VERIFY_SSL])
api = API(hass.loop, session, config[CONF_USERNAME], config[CONF_PASSWORD],
config[CONF_HOST], config[CONF_PORT], config[CONF_SSL])
scanner = TraccarScanner(api, hass, async_see)
return await scanner.async_init()
class TraccarScanner:
"""Define an object to retrieve Traccar data."""
def __init__(self, api, hass, async_see):
"""Initialize."""
self._async_see = async_see
self._api = api
self._hass = hass
async def async_init(self):
"""Further initialize connection to Traccar."""
await self._api.test_connection()
if self._api.authenticated:
await self._async_update()
async_track_time_interval(self._hass,
self._async_update,
DEFAULT_SCAN_INTERVAL)
return self._api.authenticated
async def _async_update(self, now=None):
"""Update info from Traccar."""
_LOGGER.debug('Updating device data.')
await self._api.get_device_info()
for devicename in self._api.device_info:
device = self._api.device_info[devicename]
device_attributes = {
ATTR_ADDRESS: device['address'],
ATTR_GEOFENCE: device['geofence'],
ATTR_CATEGORY: device['category'],
ATTR_TRACKER: 'traccar'
}
await self._async_see(
dev_id=slugify(device['device_id']),
gps=(device['latitude'], device['longitude']),
attributes=device_attributes)

View File

@ -61,7 +61,8 @@ class XiaomiMiioDeviceScanner(DeviceScanner):
devices = [] devices = []
try: try:
station_info = await self.hass.async_add_job(self.device.status) station_info = \
await self.hass.async_add_executor_job(self.device.status)
_LOGGER.debug("Got new station info: %s", station_info) _LOGGER.debug("Got new station info: %s", station_info)
for device in station_info.associated_stations: for device in station_info.associated_stations:

View File

@ -0,0 +1,18 @@
{
"config": {
"abort": {
"not_internet_accessible": "Va\u0161e Home Assistant instance mus\u00ed b\u00fdt p\u0159\u00edstupn\u00e1 z internetu aby mohla p\u0159ij\u00edmat zpr\u00e1vy Dialogflow.",
"one_instance_allowed": "Povolena je pouze jedna instance."
},
"create_entry": {
"default": "Chcete-li odeslat ud\u00e1losti do aplikace Home Assistant, budete muset nastavit [integraci Dialogflow]({dialogflow_url}). \n\n Vypl\u0148te n\u00e1sleduj\u00edc\u00ed informace: \n\n - URL: `{webhook_url}' \n - Metoda: POST \n - Typ obsahu: aplikace/json \n\n Podrobn\u011bj\u0161\u00ed informace naleznete v [dokumentaci]({docs_url})."
},
"step": {
"user": {
"description": "Opravdu chcete nastavit Dialogflow?",
"title": "Nastavit Dialogflow Webhook"
}
},
"title": "Dialogflow"
}
}

View File

@ -0,0 +1,10 @@
{
"config": {
"step": {
"user": {
"title": "Dialogflow Webhook einrichten"
}
},
"title": "Dialogflow"
}
}

View File

@ -0,0 +1,14 @@
{
"config": {
"abort": {
"not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Dialogflow.",
"one_instance_allowed": "Solo una instancia es necesaria."
},
"step": {
"user": {
"description": "\u00bfEst\u00e1 seguro de que desea configurar Dialogflow?"
}
},
"title": "Dialogflow"
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"not_internet_accessible": "Uw Home Assistant instantie moet toegankelijk zijn vanaf het internet om Dialogflow-berichten te ontvangen.",
"one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig."
},
"step": {
"user": {
"description": "Weet u zeker dat u Dialogflow wilt instellen?",
"title": "Stel de Twilio Dialogflow in"
}
},
"title": "Dialogflow"
}
}

View File

@ -0,0 +1,13 @@
{
"config": {
"create_entry": {
"default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u914d\u7f6e [Dialogflow \u7684 Webhook \u96c6\u6210]({dialogflow_url})\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u591a\u4fe1\u606f\u3002"
},
"step": {
"user": {
"title": "\u8bbe\u7f6e Dialogflow Webhook"
}
},
"title": "Dialogflow"
}
}

View File

@ -76,7 +76,7 @@ async def handle_webhook(hass, webhook_id, request):
async def async_setup_entry(hass, entry): async def async_setup_entry(hass, entry):
"""Configure based on config entry.""" """Configure based on config entry."""
hass.components.webhook.async_register( hass.components.webhook.async_register(
entry.data[CONF_WEBHOOK_ID], handle_webhook) DOMAIN, 'DialogFlow', entry.data[CONF_WEBHOOK_ID], handle_webhook)
return True return True

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