commit
c6c55c4419
28
.coveragerc
28
.coveragerc
|
@ -120,6 +120,9 @@ omit =
|
|||
homeassistant/components/eufy.py
|
||||
homeassistant/components/*/eufy.py
|
||||
|
||||
homeassistant/components/fibaro.py
|
||||
homeassistant/components/*/fibaro.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/lupusec.py
|
||||
homeassistant/components/*/lupusec.py
|
||||
|
||||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
|
@ -256,6 +262,10 @@ omit =
|
|||
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/light/qwikswitch.py
|
||||
|
||||
|
@ -265,7 +275,7 @@ omit =
|
|||
homeassistant/components/raincloud.py
|
||||
homeassistant/components/*/raincloud.py
|
||||
|
||||
homeassistant/components/rainmachine/*
|
||||
homeassistant/components/rainmachine/__init__.py
|
||||
homeassistant/components/*/rainmachine.py
|
||||
|
||||
homeassistant/components/raspihats.py
|
||||
|
@ -333,6 +343,9 @@ omit =
|
|||
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
|
||||
|
||||
|
@ -365,6 +378,9 @@ omit =
|
|||
|
||||
homeassistant/components/*/webostv.py
|
||||
|
||||
homeassistant/components/w800rf32.py
|
||||
homeassistant/components/*/w800rf32.py
|
||||
|
||||
homeassistant/components/wemo.py
|
||||
homeassistant/components/*/wemo.py
|
||||
|
||||
|
@ -474,6 +490,7 @@ omit =
|
|||
homeassistant/components/device_tracker/freebox.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/google_maps.py
|
||||
homeassistant/components/device_tracker/googlehome.py
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/hitron_coda.py
|
||||
homeassistant/components/device_tracker/huawei_router.py
|
||||
|
@ -496,6 +513,7 @@ omit =
|
|||
homeassistant/components/device_tracker/tile.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/traccar.py
|
||||
homeassistant/components/device_tracker/trackr.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
homeassistant/components/downloader.py
|
||||
|
@ -530,6 +548,7 @@ omit =
|
|||
homeassistant/components/light/lw12wifi.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
homeassistant/components/light/nanoleaf_aurora.py
|
||||
homeassistant/components/light/niko_home_control.py
|
||||
homeassistant/components/light/opple.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/piglow.py
|
||||
|
@ -581,6 +600,7 @@ omit =
|
|||
homeassistant/components/media_player/nadtcp.py
|
||||
homeassistant/components/media_player/onkyo.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/pandora.py
|
||||
homeassistant/components/media_player/philips_js.py
|
||||
|
@ -696,6 +716,7 @@ omit =
|
|||
homeassistant/components/sensor/fints.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/flunearyou.py
|
||||
homeassistant/components/sensor/folder.py
|
||||
homeassistant/components/sensor/foobot.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
|
@ -720,6 +741,7 @@ omit =
|
|||
homeassistant/components/sensor/kwb.py
|
||||
homeassistant/components/sensor/lacrosse.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/launch_library.py
|
||||
homeassistant/components/sensor/linky.py
|
||||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
|
@ -763,10 +785,12 @@ omit =
|
|||
homeassistant/components/sensor/rainbird.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/rtorrent.py
|
||||
homeassistant/components/sensor/ruter.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/serial.py
|
||||
homeassistant/components/sensor/seventeentrack.py
|
||||
homeassistant/components/sensor/sht31.py
|
||||
homeassistant/components/sensor/shodan.py
|
||||
homeassistant/components/sensor/sigfox.py
|
||||
|
@ -786,9 +810,11 @@ omit =
|
|||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/syncthru.py
|
||||
homeassistant/components/sensor/synologydsm.py
|
||||
homeassistant/components/sensor/srp_energy.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/sytadin.py
|
||||
homeassistant/components/sensor/tank_utility.py
|
||||
homeassistant/components/sensor/tautulli.py
|
||||
homeassistant/components/sensor/ted5000.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/thermoworks_smoke.py
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
## Checklist:
|
||||
- [ ] The code change is tested and works locally.
|
||||
- [ ] 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:
|
||||
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
|
||||
|
|
|
@ -102,6 +102,7 @@ homeassistant/components/sensor/darksky.py @fabaff
|
|||
homeassistant/components/sensor/file.py @fabaff
|
||||
homeassistant/components/sensor/filter.py @dgomes
|
||||
homeassistant/components/sensor/fixer.py @fabaff
|
||||
homeassistant/components/sensor/flunearyou.py.py @bachya
|
||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
homeassistant/components/sensor/gitter.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/jewish_calendar.py @tsvi
|
||||
homeassistant/components/sensor/linux_battery.py @fabaff
|
||||
homeassistant/components/sensor/luftdaten.py @fabaff
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/min_max.py @fabaff
|
||||
homeassistant/components/sensor/moon.py @fabaff
|
||||
|
@ -121,6 +121,7 @@ homeassistant/components/sensor/pvoutput.py @fabaff
|
|||
homeassistant/components/sensor/qnap.py @colinodell
|
||||
homeassistant/components/sensor/scrape.py @fabaff
|
||||
homeassistant/components/sensor/serial.py @fabaff
|
||||
homeassistant/components/sensor/seventeentrack.py @bachya
|
||||
homeassistant/components/sensor/shodan.py @fabaff
|
||||
homeassistant/components/sensor/sma.py @kellerza
|
||||
homeassistant/components/sensor/sql.py @dgomes
|
||||
|
@ -189,6 +190,8 @@ homeassistant/components/*/konnected.py @heythisisnate
|
|||
# L
|
||||
homeassistant/components/lifx.py @amelchio
|
||||
homeassistant/components/*/lifx.py @amelchio
|
||||
homeassistant/components/luftdaten/* @fabaff
|
||||
homeassistant/components/*/luftdaten.py @fabaff
|
||||
|
||||
# M
|
||||
homeassistant/components/matrix.py @tinloaf
|
||||
|
|
|
@ -13,6 +13,7 @@ from homeassistant.core import callback, HomeAssistant
|
|||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import auth_store, models
|
||||
from .const import GROUP_ID_ADMIN
|
||||
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
|
||||
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
|
||||
|
||||
|
@ -117,6 +118,10 @@ class AuthManager:
|
|||
"""Retrieve a user."""
|
||||
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(
|
||||
self, credentials: models.Credentials) -> Optional[models.User]:
|
||||
"""Get a user by credential, return None if not found."""
|
||||
|
@ -127,13 +132,15 @@ class AuthManager:
|
|||
|
||||
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."""
|
||||
user = await self._store.async_create_user(
|
||||
name=name,
|
||||
system_generated=True,
|
||||
is_active=True,
|
||||
groups=[],
|
||||
group_ids=group_ids or [],
|
||||
)
|
||||
|
||||
self.hass.bus.async_fire(EVENT_USER_ADDED, {
|
||||
|
@ -144,11 +151,10 @@ class AuthManager:
|
|||
|
||||
async def async_create_user(self, name: str) -> models.User:
|
||||
"""Create a user."""
|
||||
group = (await self._store.async_get_groups())[0]
|
||||
kwargs = {
|
||||
'name': name,
|
||||
'is_active': True,
|
||||
'groups': [group]
|
||||
'group_ids': [GROUP_ID_ADMIN]
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if await self._user_should_be_owner():
|
||||
|
@ -213,6 +219,17 @@ class AuthManager:
|
|||
'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:
|
||||
"""Activate a user."""
|
||||
await self._store.async_activate_user(user)
|
||||
|
|
|
@ -10,11 +10,14 @@ from homeassistant.core import HomeAssistant, callback
|
|||
from homeassistant.util import dt as dt_util
|
||||
|
||||
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_KEY = 'auth'
|
||||
INITIAL_GROUP_NAME = 'All Access'
|
||||
GROUP_NAME_ADMIN = 'Administrators'
|
||||
GROUP_NAME_READ_ONLY = 'Read Only'
|
||||
|
||||
|
||||
class AuthStore:
|
||||
|
@ -42,6 +45,14 @@ class AuthStore:
|
|||
|
||||
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]:
|
||||
"""Retrieve all users."""
|
||||
if self._users is None:
|
||||
|
@ -63,7 +74,7 @@ class AuthStore:
|
|||
is_active: Optional[bool] = None,
|
||||
system_generated: Optional[bool] = 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."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
@ -71,11 +82,18 @@ class AuthStore:
|
|||
assert self._users 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 = {
|
||||
'name': name,
|
||||
# Until we get group management, we just put everyone in the
|
||||
# same group.
|
||||
'groups': groups or [],
|
||||
'groups': groups,
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if is_owner is not None:
|
||||
|
@ -115,6 +133,33 @@ class AuthStore:
|
|||
self._users.pop(user.id)
|
||||
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:
|
||||
"""Activate a user."""
|
||||
user.is_active = True
|
||||
|
@ -238,38 +283,98 @@ class AuthStore:
|
|||
users = OrderedDict() # type: Dict[str, models.User]
|
||||
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
|
||||
# was added.
|
||||
|
||||
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(
|
||||
name=group_dict['name'],
|
||||
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:
|
||||
migrate_group = models.Group(
|
||||
name=INITIAL_GROUP_NAME,
|
||||
policy=DEFAULT_POLICY
|
||||
)
|
||||
groups[migrate_group.id] = migrate_group
|
||||
# If we find a no_policy_group, we need to migrate all users to the
|
||||
# admin group. We only do this if there are no other groups, as is
|
||||
# the expected state. If not expected state, not marking people admin.
|
||||
# This is part of migrating from state 1
|
||||
if groups and group_without_policy is not None:
|
||||
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']:
|
||||
# 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(
|
||||
name=user_dict['name'],
|
||||
groups=[groups[group_id] for group_id
|
||||
in user_dict.get('group_ids', [])],
|
||||
groups=user_groups,
|
||||
id=user_dict['id'],
|
||||
is_owner=user_dict['is_owner'],
|
||||
is_active=user_dict['is_active'],
|
||||
system_generated=user_dict['system_generated'],
|
||||
)
|
||||
if migrate_group is not None and not user_dict['system_generated']:
|
||||
users[user_dict['id']].groups = [migrate_group]
|
||||
|
||||
for cred_dict in data['credentials']:
|
||||
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
||||
|
@ -356,11 +461,11 @@ class AuthStore:
|
|||
groups = []
|
||||
for group in self._groups.values():
|
||||
g_dict = {
|
||||
'name': group.name,
|
||||
'id': group.id,
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if group.policy is not DEFAULT_POLICY:
|
||||
if group.id not in (GROUP_ID_READ_ONLY, GROUP_ID_ADMIN):
|
||||
g_dict['name'] = group.name
|
||||
g_dict['policy'] = group.policy
|
||||
|
||||
groups.append(g_dict)
|
||||
|
@ -410,13 +515,29 @@ class AuthStore:
|
|||
"""Set default values for auth store."""
|
||||
self._users = OrderedDict() # type: Dict[str, models.User]
|
||||
|
||||
# Add default group
|
||||
all_access_group = models.Group(
|
||||
name=INITIAL_GROUP_NAME,
|
||||
policy=DEFAULT_POLICY,
|
||||
groups = OrderedDict() # type: Dict[str, models.Group]
|
||||
admin_group = _system_admin_group()
|
||||
groups[admin_group.id] = admin_group
|
||||
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,
|
||||
)
|
||||
|
|
|
@ -3,3 +3,6 @@ from datetime import timedelta
|
|||
|
||||
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||
MFA_SESSION_EXPIRATION = timedelta(minutes=5)
|
||||
|
||||
GROUP_ID_ADMIN = 'system-admin'
|
||||
GROUP_ID_READ_ONLY = 'system-read-only'
|
||||
|
|
|
@ -8,6 +8,7 @@ import attr
|
|||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import permissions as perm_mdl
|
||||
from .const import GROUP_ID_ADMIN
|
||||
from .util import generate_secret
|
||||
|
||||
TOKEN_TYPE_NORMAL = 'normal'
|
||||
|
@ -22,6 +23,7 @@ class Group:
|
|||
name = attr.ib(type=str) # type: Optional[str]
|
||||
policy = attr.ib(type=perm_mdl.PolicyType)
|
||||
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
||||
system_generated = attr.ib(type=bool, default=False)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
|
@ -47,7 +49,7 @@ class User:
|
|||
) # type: Dict[str, RefreshToken]
|
||||
|
||||
_permissions = attr.ib(
|
||||
type=perm_mdl.PolicyPermissions,
|
||||
type=Optional[perm_mdl.PolicyPermissions],
|
||||
init=False,
|
||||
cmp=False,
|
||||
default=None,
|
||||
|
@ -68,6 +70,19 @@ class User:
|
|||
|
||||
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)
|
||||
class RefreshToken:
|
||||
|
|
|
@ -5,20 +5,11 @@ from typing import ( # noqa: F401
|
|||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import State
|
||||
|
||||
from .common import CategoryType, PolicyType
|
||||
from .const import CAT_ENTITIES
|
||||
from .types import PolicyType
|
||||
from .entities import ENTITY_POLICY_SCHEMA, compile_entities
|
||||
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({
|
||||
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
|
||||
})
|
||||
|
@ -29,13 +20,20 @@ _LOGGER = logging.getLogger(__name__)
|
|||
class AbstractPermissions:
|
||||
"""Default permissions class."""
|
||||
|
||||
def check_entity(self, entity_id: str, key: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
_cached_entity_func = None
|
||||
|
||||
def _entity_func(self) -> Callable[[str, str], bool]:
|
||||
"""Return a function that can test entity access."""
|
||||
raise NotImplementedError
|
||||
|
||||
def filter_states(self, states: List[State]) -> List[State]:
|
||||
"""Filter a list of states for what the user is allowed to see."""
|
||||
raise NotImplementedError
|
||||
def check_entity(self, entity_id: str, key: str) -> bool:
|
||||
"""Check if we can access entity."""
|
||||
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):
|
||||
|
@ -44,34 +42,10 @@ class PolicyPermissions(AbstractPermissions):
|
|||
def __init__(self, policy: PolicyType) -> None:
|
||||
"""Initialize the permission class."""
|
||||
self._policy = policy
|
||||
self._compiled = {} # type: Dict[str, Callable[..., bool]]
|
||||
|
||||
def check_entity(self, entity_id: str, key: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
func = self._policy_func(CAT_ENTITIES, compile_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 _entity_func(self) -> Callable[[str, str], bool]:
|
||||
"""Return a function that can test entity access."""
|
||||
return compile_entities(self._policy.get(CAT_ENTITIES))
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Equals check."""
|
||||
|
@ -85,13 +59,9 @@ class _OwnerPermissions(AbstractPermissions):
|
|||
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
def check_entity(self, entity_id: str, key: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
return True
|
||||
|
||||
def filter_states(self, states: List[State]) -> List[State]:
|
||||
"""Filter a list of states for what the user is allowed to see."""
|
||||
return states
|
||||
def _entity_func(self) -> Callable[[str, str], bool]:
|
||||
"""Return a function that can test entity access."""
|
||||
return lambda entity_id, key: True
|
||||
|
||||
|
||||
OwnerPermissions = _OwnerPermissions() # pylint: disable=invalid-name
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
"""Permission constants."""
|
||||
CAT_ENTITIES = 'entities'
|
||||
SUBCAT_ALL = 'all'
|
||||
|
||||
POLICY_READ = 'read'
|
||||
POLICY_CONTROL = 'control'
|
||||
POLICY_EDIT = 'edit'
|
|
@ -5,12 +5,8 @@ from typing import ( # noqa: F401
|
|||
|
||||
import voluptuous as vol
|
||||
|
||||
from .common import CategoryType, ValueType, SUBCAT_ALL
|
||||
|
||||
|
||||
POLICY_READ = 'read'
|
||||
POLICY_CONTROL = 'control'
|
||||
POLICY_EDIT = 'edit'
|
||||
from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT
|
||||
from .types import CategoryType, ValueType
|
||||
|
||||
SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
|
||||
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]:
|
||||
"""Test if an entity is allowed based on the keys."""
|
||||
if schema is None or isinstance(schema, bool):
|
||||
return schema
|
||||
assert isinstance(schema, dict)
|
||||
return schema.get(keys[0])
|
||||
return schema.get(key)
|
||||
|
||||
|
||||
def compile_entities(policy: CategoryType) \
|
||||
-> Callable[[str, Tuple[str]], bool]:
|
||||
-> Callable[[str, str], bool]:
|
||||
"""Compile policy into a function that tests policy."""
|
||||
# None, Empty Dict, False
|
||||
if not policy:
|
||||
def apply_policy_deny_all(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
def apply_policy_deny_all(entity_id: str, key: str) -> bool:
|
||||
"""Decline all."""
|
||||
return False
|
||||
|
||||
return apply_policy_deny_all
|
||||
|
||||
if policy is True:
|
||||
def apply_policy_allow_all(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
def apply_policy_allow_all(entity_id: str, key: str) -> bool:
|
||||
"""Approve all."""
|
||||
return True
|
||||
|
||||
|
@ -65,7 +61,7 @@ def compile_entities(policy: CategoryType) \
|
|||
entity_ids = policy.get(ENTITY_ENTITY_IDS)
|
||||
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.
|
||||
# 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
|
||||
# So return right away.
|
||||
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."""
|
||||
return entity_ids # type: ignore
|
||||
|
||||
return allowed_entity_id_bool
|
||||
|
||||
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]:
|
||||
"""Test if allowed entity_id."""
|
||||
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)
|
||||
|
||||
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]:
|
||||
"""Test if allowed domain."""
|
||||
return domains
|
||||
|
@ -98,31 +94,31 @@ def compile_entities(policy: CategoryType) \
|
|||
funcs.append(allowed_domain_bool)
|
||||
|
||||
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]:
|
||||
"""Test if allowed domain."""
|
||||
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)
|
||||
|
||||
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]:
|
||||
"""Test if allowed domain."""
|
||||
return all_entities
|
||||
funcs.append(allowed_all_entities_bool)
|
||||
|
||||
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]:
|
||||
"""Test if allowed domain."""
|
||||
return _entity_allowed(all_entities, keys)
|
||||
return _entity_allowed(all_entities, key)
|
||||
funcs.append(allowed_all_entities_dict)
|
||||
|
||||
# Can happen if no valid subcategories specified
|
||||
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."""
|
||||
return False
|
||||
|
||||
|
@ -132,16 +128,16 @@ def compile_entities(policy: CategoryType) \
|
|||
func = funcs[0]
|
||||
|
||||
@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."""
|
||||
return func(entity_id, keys) is True
|
||||
return func(entity_id, key) is True
|
||||
|
||||
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."""
|
||||
for func in funcs:
|
||||
result = func(entity_id, keys)
|
||||
result = func(entity_id, key)
|
||||
if result is not None:
|
||||
return result
|
||||
return False
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from typing import ( # noqa: F401
|
||||
cast, Dict, List, Set)
|
||||
|
||||
from .common import PolicyType, CategoryType
|
||||
from .types import PolicyType, CategoryType
|
||||
|
||||
|
||||
def merge_policies(policies: List[PolicyType]) -> PolicyType:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,5 +29,3 @@ CategoryType = Union[
|
|||
|
||||
# Example: { entities: … }
|
||||
PolicyType = Mapping[str, CategoryType]
|
||||
|
||||
SUBCAT_ALL = 'all'
|
|
@ -13,9 +13,10 @@ from homeassistant.const import (
|
|||
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."""
|
||||
add_entities([
|
||||
async_add_entities([
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', None, False, {
|
||||
STATE_ALARM_ARMED_AWAY: {
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
|
|
|
@ -12,10 +12,10 @@ import homeassistant.components.alarm_control_panel as alarm
|
|||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
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
|
||||
|
||||
REQUIREMENTS = ['pyialarm==0.2']
|
||||
REQUIREMENTS = ['pyialarm==0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -89,6 +89,8 @@ class IAlarmPanel(alarm.AlarmControlPanel):
|
|||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif status == self._client.ARMED_STAY:
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif status == self._client.TRIGGERED:
|
||||
state = STATE_ALARM_TRIGGERED
|
||||
else:
|
||||
state = None
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -335,11 +335,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
|||
|
||||
return state_attr
|
||||
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe to MQTT events.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to MQTT events."""
|
||||
async_track_state_change(
|
||||
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)
|
||||
return
|
||||
|
||||
return mqtt.async_subscribe(
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._command_topic, message_received, self._qos)
|
||||
|
||||
async def _async_state_changed_listener(self, entity_id, old_state,
|
||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.const import (
|
|||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.20']
|
||||
REQUIREMENTS = ['total_connect_client==0.22']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -16,9 +16,9 @@ from homeassistant.components import (
|
|||
input_boolean, light, lock, media_player, scene, script, sensor, switch)
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, SERVICE_LOCK,
|
||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||
ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CLOUD_NEVER_EXPOSED_ENTITIES,
|
||||
CONF_NAME, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
|
||||
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON, STATE_UNLOCKED,
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
|
@ -474,6 +474,26 @@ class _AlexaColorController(_AlexaInterface):
|
|||
def name(self):
|
||||
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):
|
||||
"""Implements Alexa.ColorTemperatureController.
|
||||
|
@ -717,6 +737,9 @@ class _ClimateCapabilities(_AlexaEntity):
|
|||
return [_DisplayCategory.THERMOSTAT]
|
||||
|
||||
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 _AlexaTemperatureSensor(self.hass, self.entity)
|
||||
|
||||
|
@ -1194,6 +1217,11 @@ async def async_api_discovery(hass, config, directive, context):
|
|||
discovery_endpoints = []
|
||||
|
||||
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):
|
||||
_LOGGER.debug("Not exposing %s because filtered by config",
|
||||
entity.entity_id)
|
||||
|
@ -1205,7 +1233,7 @@ async def async_api_discovery(hass, config, directive, context):
|
|||
|
||||
endpoint = {
|
||||
'displayCategories': alexa_entity.display_categories(),
|
||||
'additionalApplianceDetails': {},
|
||||
'cookie': {},
|
||||
'endpointId': alexa_entity.entity_id(),
|
||||
'friendlyName': alexa_entity.friendly_name(),
|
||||
'description': alexa_entity.description(),
|
||||
|
|
|
@ -20,7 +20,8 @@ from homeassistant.const import (
|
|||
URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM,
|
||||
URL_API_TEMPLATE, __version__)
|
||||
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.service import async_get_all_descriptions
|
||||
from homeassistant.helpers.state import AsyncTrackStates
|
||||
|
@ -81,6 +82,8 @@ class APIEventStream(HomeAssistantView):
|
|||
|
||||
async def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized()
|
||||
hass = request.app['hass']
|
||||
stop_obj = object()
|
||||
to_write = asyncio.Queue(loop=hass.loop)
|
||||
|
@ -185,7 +188,13 @@ class APIStatesView(HomeAssistantView):
|
|||
@ha.callback
|
||||
def get(self, request):
|
||||
"""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):
|
||||
|
@ -197,6 +206,10 @@ class APIEntityStateView(HomeAssistantView):
|
|||
@ha.callback
|
||||
def get(self, request, entity_id):
|
||||
"""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)
|
||||
if state:
|
||||
return self.json(state)
|
||||
|
@ -204,6 +217,8 @@ class APIEntityStateView(HomeAssistantView):
|
|||
|
||||
async def post(self, request, entity_id):
|
||||
"""Update state of entity."""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized(entity_id=entity_id)
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = await request.json()
|
||||
|
@ -236,6 +251,8 @@ class APIEntityStateView(HomeAssistantView):
|
|||
@ha.callback
|
||||
def delete(self, request, entity_id):
|
||||
"""Remove entity."""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized(entity_id=entity_id)
|
||||
if request.app['hass'].states.async_remove(entity_id):
|
||||
return self.json_message("Entity removed.")
|
||||
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
|
||||
|
@ -261,6 +278,8 @@ class APIEventView(HomeAssistantView):
|
|||
|
||||
async def post(self, request, event_type):
|
||||
"""Fire events."""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized()
|
||||
body = await request.text()
|
||||
try:
|
||||
event_data = json.loads(body) if body else None
|
||||
|
@ -346,6 +365,8 @@ class APITemplateView(HomeAssistantView):
|
|||
|
||||
async def post(self, request):
|
||||
"""Render a template."""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized()
|
||||
try:
|
||||
data = await request.json()
|
||||
tpl = template.Template(data['template'], request.app['hass'])
|
||||
|
@ -363,6 +384,8 @@ class APIErrorLog(HomeAssistantView):
|
|||
|
||||
async def get(self, request):
|
||||
"""Retrieve API error log."""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized()
|
||||
return web.FileResponse(request.app['hass'].data[DATA_LOGGING])
|
||||
|
||||
|
||||
|
|
|
@ -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
|
|
@ -11,8 +11,9 @@ import voluptuous as vol
|
|||
from requests import RequestException
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import callback
|
||||
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.util import Throttle
|
||||
|
||||
|
@ -20,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
_CONFIGURING = {}
|
||||
|
||||
REQUIREMENTS = ['py-august==0.6.0']
|
||||
REQUIREMENTS = ['py-august==0.7.0']
|
||||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
ACTIVITY_FETCH_LIMIT = 10
|
||||
|
@ -116,7 +117,8 @@ def setup_august(hass, config, api, authenticator):
|
|||
if DOMAIN in _CONFIGURING:
|
||||
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:
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
@ -136,9 +138,16 @@ def setup(hass, config):
|
|||
"""Set up the August component."""
|
||||
from august.api import Api
|
||||
from august.authenticator import Authenticator
|
||||
from requests import Session
|
||||
|
||||
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(
|
||||
api,
|
||||
|
@ -154,8 +163,9 @@ def setup(hass, config):
|
|||
class AugustData:
|
||||
"""August data object."""
|
||||
|
||||
def __init__(self, api, access_token):
|
||||
def __init__(self, hass, api, access_token):
|
||||
"""Init August data object."""
|
||||
self._hass = hass
|
||||
self._api = api
|
||||
self._access_token = access_token
|
||||
self._doorbells = self._api.get_doorbells(self._access_token) or []
|
||||
|
@ -168,6 +178,22 @@ class AugustData:
|
|||
self._door_state_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
|
||||
def house_ids(self):
|
||||
"""Return a list of house_ids."""
|
||||
|
@ -201,8 +227,11 @@ class AugustData:
|
|||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
||||
"""Update data object with latest from August API."""
|
||||
_LOGGER.debug("Updating device activities")
|
||||
_LOGGER.debug("Start retrieving device activities")
|
||||
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,
|
||||
house_id,
|
||||
limit=limit)
|
||||
|
@ -211,6 +240,7 @@ class AugustData:
|
|||
for device_id in device_ids:
|
||||
self._activities_by_id[device_id] = [a for a in activities if
|
||||
a.device_id == device_id]
|
||||
_LOGGER.debug("Completed retrieving device activities")
|
||||
|
||||
def get_doorbell_detail(self, doorbell_id):
|
||||
"""Return doorbell detail."""
|
||||
|
@ -223,7 +253,7 @@ class AugustData:
|
|||
|
||||
_LOGGER.debug("Start retrieving doorbell details")
|
||||
for doorbell in self._doorbells:
|
||||
_LOGGER.debug("Updating status for %s",
|
||||
_LOGGER.debug("Updating doorbell status for %s",
|
||||
doorbell.device_name)
|
||||
try:
|
||||
detail_by_id[doorbell.device_id] =\
|
||||
|
@ -267,7 +297,7 @@ class AugustData:
|
|||
|
||||
_LOGGER.debug("Start retrieving door status")
|
||||
for lock in self._locks:
|
||||
_LOGGER.debug("Updating status for %s",
|
||||
_LOGGER.debug("Updating door status for %s",
|
||||
lock.device_name)
|
||||
|
||||
try:
|
||||
|
@ -291,7 +321,7 @@ class AugustData:
|
|||
|
||||
_LOGGER.debug("Start retrieving locks status")
|
||||
for lock in self._locks:
|
||||
_LOGGER.debug("Updating status for %s",
|
||||
_LOGGER.debug("Updating lock status for %s",
|
||||
lock.device_name)
|
||||
try:
|
||||
status_by_id[lock.device_id] = self._api.get_lock_status(
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,27 @@
|
|||
{
|
||||
"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": {
|
||||
"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": {
|
||||
"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} ` **.",
|
||||
|
|
|
@ -400,6 +400,9 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action):
|
|||
This method is a coroutine.
|
||||
"""
|
||||
removes = []
|
||||
info = {
|
||||
'name': name
|
||||
}
|
||||
|
||||
for conf in trigger_configs:
|
||||
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:
|
||||
return None
|
||||
|
||||
remove = await platform.async_trigger(hass, conf, action)
|
||||
remove = await platform.async_trigger(hass, conf, action, info)
|
||||
|
||||
if not remove:
|
||||
_LOGGER.error("Error setting up trigger %s", name)
|
||||
|
|
|
@ -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."""
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
event_data_schema = vol.Schema(
|
||||
|
|
|
@ -33,7 +33,7 @@ def source_match(state, 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."""
|
||||
source = config.get(CONF_SOURCE).lower()
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
|
|
|
@ -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."""
|
||||
event = config.get(CONF_EVENT)
|
||||
|
||||
|
|
|
@ -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."""
|
||||
number = config.get(CONF_NUMBER)
|
||||
held_more_than = config.get(CONF_HELD_MORE_THAN)
|
||||
|
|
|
@ -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."""
|
||||
topic = config.get(CONF_TOPIC)
|
||||
payload = config.get(CONF_PAYLOAD)
|
||||
|
|
|
@ -28,7 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
|||
_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."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
below = config.get(CONF_BELOW)
|
||||
|
|
|
@ -26,7 +26,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
|||
}), 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."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
|
|
|
@ -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."""
|
||||
event = config.get(CONF_EVENT)
|
||||
offset = config.get(CONF_OFFSET)
|
||||
|
|
|
@ -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."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template.hass = hass
|
||||
|
|
|
@ -28,7 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
|||
}), 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."""
|
||||
if CONF_AT in config:
|
||||
at_time = config.get(CONF_AT)
|
||||
|
|
|
@ -14,6 +14,8 @@ from homeassistant.core import callback
|
|||
from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import DOMAIN as AUTOMATION_DOMAIN
|
||||
|
||||
DEPENDENCIES = ('webhook',)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -39,10 +41,11 @@ async def _handle_webhook(action, hass, webhook_id, request):
|
|||
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."""
|
||||
webhook_id = config.get(CONF_WEBHOOK_ID)
|
||||
hass.components.webhook.async_register(
|
||||
AUTOMATION_DOMAIN, automation_info['name'],
|
||||
webhook_id, partial(_handle_webhook, action))
|
||||
|
||||
@callback
|
||||
|
|
|
@ -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."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
|
|
|
@ -6,8 +6,8 @@ https://home-assistant.io/components/binary_sensor.deconz/
|
|||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz.const import (
|
||||
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
|
||||
DECONZ_DOMAIN)
|
||||
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DECONZ_REACHABLE,
|
||||
DOMAIN as DECONZ_DOMAIN)
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.core import callback
|
||||
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):
|
||||
"""Set up the deCONZ binary sensor."""
|
||||
gateway = hass.data[DECONZ_DOMAIN]
|
||||
|
||||
@callback
|
||||
def async_add_sensor(sensors):
|
||||
"""Add binary sensor from deCONZ."""
|
||||
|
@ -33,30 +35,35 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
for sensor in sensors:
|
||||
if sensor.type in DECONZ_BINARY_SENSOR and \
|
||||
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
|
||||
entities.append(DeconzBinarySensor(sensor))
|
||||
entities.append(DeconzBinarySensor(sensor, gateway))
|
||||
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_add_sensor(hass.data[DATA_DECONZ].api.sensors.values())
|
||||
async_add_sensor(gateway.api.sensors.values())
|
||||
|
||||
|
||||
class DeconzBinarySensor(BinarySensorDevice):
|
||||
"""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."""
|
||||
self._sensor = sensor
|
||||
self.gateway = gateway
|
||||
self.unsub_dispatcher = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe sensors events."""
|
||||
self._sensor.register_async_callback(self.async_update_callback)
|
||||
self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
|
||||
self._sensor.deconz_id
|
||||
self.gateway.deconz_ids[self.entity_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:
|
||||
"""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 = None
|
||||
|
||||
|
@ -101,7 +108,7 @@ class DeconzBinarySensor(BinarySensorDevice):
|
|||
@property
|
||||
def available(self):
|
||||
"""Return True if sensor is available."""
|
||||
return self._sensor.reachable
|
||||
return self.gateway.available and self._sensor.reachable
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -128,7 +135,7 @@ class DeconzBinarySensor(BinarySensorDevice):
|
|||
self._sensor.uniqueid.count(':') != 7):
|
||||
return None
|
||||
serial = self._sensor.uniqueid.split('-', 1)[0]
|
||||
bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid
|
||||
bridgeid = self.gateway.api.config.bridgeid
|
||||
return {
|
||||
'connections': {(CONNECTION_ZIGBEE, serial)},
|
||||
'identifiers': {(DECONZ_DOMAIN, serial)},
|
||||
|
|
|
@ -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
|
|
@ -57,7 +57,8 @@ class InsteonBinarySensor(InsteonEntity, BinarySensorDevice):
|
|||
"""Return the boolean response if the node is on."""
|
||||
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 on_val
|
||||
|
|
|
@ -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
|
|
@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/binary_sensor.mqtt/
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -19,7 +18,8 @@ from homeassistant.const import (
|
|||
from homeassistant.components.mqtt import (
|
||||
ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC,
|
||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
|
||||
MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo)
|
||||
MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
||||
subscription)
|
||||
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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
|
||||
|
||||
async_add_entities([MqttBinarySensor(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_DEVICE_CLASS),
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_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,
|
||||
config,
|
||||
discovery_hash
|
||||
)])
|
||||
|
||||
|
||||
|
@ -101,35 +88,71 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
|
|||
MqttEntityDeviceInfo, BinarySensorDevice):
|
||||
"""Representation a binary sensor that is updated by MQTT."""
|
||||
|
||||
def __init__(self, name, state_topic, availability_topic, device_class,
|
||||
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):
|
||||
def __init__(self, config, discovery_hash):
|
||||
"""Initialize the MQTT binary sensor."""
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash)
|
||||
MqttEntityDeviceInfo.__init__(self, device_config)
|
||||
self._name = name
|
||||
self._config = config
|
||||
self._state = None
|
||||
self._state_topic = state_topic
|
||||
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._sub_state = 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):
|
||||
"""Subscribe mqtt events."""
|
||||
await MqttAvailability.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
|
||||
def off_delay_listener(now):
|
||||
"""Switch device off after a delay."""
|
||||
|
@ -163,8 +186,16 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
|
|||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, state_message_received, self._qos)
|
||||
self._sub_state = await subscription.async_subscribe_topics(
|
||||
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
|
||||
def should_poll(self):
|
||||
|
|
|
@ -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
|
|
@ -8,28 +8,29 @@ import logging
|
|||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.rainmachine import (
|
||||
BINARY_SENSORS, DATA_RAINMACHINE, SENSOR_UPDATE_TOPIC, TYPE_FREEZE,
|
||||
TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH,
|
||||
TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity)
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
BINARY_SENSORS, DATA_CLIENT, DOMAIN as RAINMACHINE_DOMAIN,
|
||||
SENSOR_UPDATE_TOPIC, TYPE_FREEZE, TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS,
|
||||
TYPE_HOURLY, TYPE_MONTH, TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY,
|
||||
RainMachineEntity)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['rainmachine']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the RainMachine Switch platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
"""Set up RainMachine binary sensors based on the old way."""
|
||||
pass
|
||||
|
||||
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 = []
|
||||
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
|
||||
for sensor_type in rainmachine.binary_sensor_conditions:
|
||||
name, icon = BINARY_SENSORS[sensor_type]
|
||||
binary_sensors.append(
|
||||
RainMachineBinarySensor(rainmachine, sensor_type, name, icon))
|
||||
|
@ -70,15 +71,15 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
|
|||
return '{0}_{1}'.format(
|
||||
self.rainmachine.device_mac.replace(':', ''), self._sensor_type)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@callback
|
||||
def _update_data(self):
|
||||
def update(self):
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SENSOR_UPDATE_TOPIC, self._update_data)
|
||||
self._dispatcher_handlers.append(async_dispatcher_connect(
|
||||
self.hass, SENSOR_UPDATE_TOPIC, update))
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the state."""
|
||||
|
|
|
@ -60,7 +60,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
data = hass.data[SENSE_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)
|
||||
|
||||
|
||||
|
|
|
@ -58,10 +58,12 @@ async def async_setup_platform(hass, config, async_add_entities,
|
|||
entity_ids = set()
|
||||
manual_entity_ids = device_config.get(ATTR_ENTITY_ID)
|
||||
|
||||
for template in (
|
||||
value_template,
|
||||
icon_template,
|
||||
entity_picture_template,
|
||||
invalid_templates = []
|
||||
|
||||
for tpl_name, template in (
|
||||
(CONF_VALUE_TEMPLATE, value_template),
|
||||
(CONF_ICON_TEMPLATE, icon_template),
|
||||
(CONF_ENTITY_PICTURE_TEMPLATE, entity_picture_template),
|
||||
):
|
||||
if template is None:
|
||||
continue
|
||||
|
@ -73,6 +75,8 @@ async def async_setup_platform(hass, config, async_add_entities,
|
|||
template_entity_ids = template.extract_entities()
|
||||
if template_entity_ids == MATCH_ALL:
|
||||
entity_ids = MATCH_ALL
|
||||
# Cut off _template from name
|
||||
invalid_templates.append(tpl_name[:-9])
|
||||
elif entity_ids != MATCH_ALL:
|
||||
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:
|
||||
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)
|
||||
device_class = device_config.get(CONF_DEVICE_CLASS)
|
||||
delay_on = device_config.get(CONF_DELAY_ON)
|
||||
|
@ -132,10 +144,12 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
@callback
|
||||
def template_bsensor_startup(event):
|
||||
"""Update template on startup."""
|
||||
if self._entities != MATCH_ALL:
|
||||
# Track state change only for valid templates
|
||||
async_track_state_change(
|
||||
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(
|
||||
EVENT_HOMEASSISTANT_START, template_bsensor_startup)
|
||||
|
@ -233,3 +247,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
async_track_same_state(
|
||||
self.hass, period, set_state, entity_ids=self._entities,
|
||||
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()
|
||||
|
|
|
@ -22,7 +22,7 @@ from homeassistant.helpers.entity import generate_entity_id
|
|||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
REQUIREMENTS = ['numpy==1.15.3']
|
||||
REQUIREMENTS = ['numpy==1.15.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -209,7 +209,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
|||
else:
|
||||
self._should_poll = True
|
||||
if self.entity_id is not None:
|
||||
self._hass.bus.fire('motion', {
|
||||
self._hass.bus.fire('xiaomi_aqara.motion', {
|
||||
'entity_id': self.entity_id
|
||||
})
|
||||
|
||||
|
@ -417,7 +417,7 @@ class XiaomiButton(XiaomiBinarySensor):
|
|||
_LOGGER.warning("Unsupported click_type detected: %s", value)
|
||||
return False
|
||||
|
||||
self._hass.bus.fire('click', {
|
||||
self._hass.bus.fire('xiaomi_aqara.click', {
|
||||
'entity_id': self.entity_id,
|
||||
'click_type': click_type
|
||||
})
|
||||
|
@ -453,14 +453,14 @@ class XiaomiCube(XiaomiBinarySensor):
|
|||
def parse_data(self, data, raw_data):
|
||||
"""Parse data sent by gateway."""
|
||||
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,
|
||||
'action_type': data[self._data_key]
|
||||
})
|
||||
self._last_action = data[self._data_key]
|
||||
|
||||
if 'rotate' in data:
|
||||
self._hass.bus.fire('cube_action', {
|
||||
self._hass.bus.fire('xiaomi_aqara.cube_action', {
|
||||
'entity_id': self.entity_id,
|
||||
'action_type': 'rotate',
|
||||
'action_value': float(data['rotate'].replace(",", "."))
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
|
||||
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
|
||||
|
||||
REQUIREMENTS = ['blinkpy==0.10.1']
|
||||
REQUIREMENTS = ['blinkpy==0.10.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -89,13 +89,12 @@ class MqttCamera(Camera):
|
|||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe MQTT events."""
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""Handle new MQTT messages."""
|
||||
self._last_image = payload
|
||||
|
||||
return mqtt.async_subscribe(
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic, message_received, self._qos, None)
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
|
@ -249,9 +249,11 @@ class ClimateDevice(Entity):
|
|||
self.hass, self.target_temperature_low, self.temperature_unit,
|
||||
self.precision)
|
||||
|
||||
if self.current_humidity is not None:
|
||||
data[ATTR_CURRENT_HUMIDITY] = self.current_humidity
|
||||
|
||||
if supported_features & SUPPORT_TARGET_HUMIDITY:
|
||||
data[ATTR_HUMIDITY] = self.target_humidity
|
||||
data[ATTR_CURRENT_HUMIDITY] = self.current_humidity
|
||||
|
||||
if supported_features & SUPPORT_TARGET_HUMIDITY_LOW:
|
||||
data[ATTR_MIN_HUMIDITY] = self.min_humidity
|
||||
|
|
|
@ -22,7 +22,7 @@ from homeassistant.const import (
|
|||
ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pydaikin==0.6']
|
||||
REQUIREMENTS = ['pydaikin==0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -82,7 +82,6 @@ class DaikinClimate(ClimateDevice):
|
|||
from pydaikin import appliance
|
||||
|
||||
self._api = api
|
||||
self._force_refresh = False
|
||||
self._list = {
|
||||
ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN),
|
||||
ATTR_FAN_MODE: list(
|
||||
|
@ -102,19 +101,11 @@ class DaikinClimate(ClimateDevice):
|
|||
self._supported_features = SUPPORT_TARGET_TEMPERATURE \
|
||||
| SUPPORT_OPERATION_MODE
|
||||
|
||||
daikin_attr = HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE]
|
||||
if self._api.device.values.get(daikin_attr) is not None:
|
||||
if self._api.device.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.values.get(daikin_attr) is not None:
|
||||
if self._api.device.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):
|
||||
"""Retrieve device settings from API library cache."""
|
||||
|
@ -189,7 +180,6 @@ class DaikinClimate(ClimateDevice):
|
|||
_LOGGER.error("Invalid temperature %s", value)
|
||||
|
||||
if values:
|
||||
self._force_refresh = True
|
||||
self._api.device.set(values)
|
||||
|
||||
@property
|
||||
|
@ -270,5 +260,4 @@ class DaikinClimate(ClimateDevice):
|
|||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._api.update(no_throttle=self._force_refresh)
|
||||
self._force_refresh = False
|
||||
self._api.update()
|
||||
|
|
|
@ -17,7 +17,8 @@ from homeassistant.components.climate import (
|
|||
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
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.event import (
|
||||
async_track_state_change, async_track_time_interval)
|
||||
|
@ -43,6 +44,7 @@ CONF_HOT_TOLERANCE = 'hot_tolerance'
|
|||
CONF_KEEP_ALIVE = 'keep_alive'
|
||||
CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode'
|
||||
CONF_AWAY_TEMP = 'away_temp'
|
||||
CONF_PRECISION = 'precision'
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_OPERATION_MODE)
|
||||
|
||||
|
@ -63,7 +65,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_INITIAL_OPERATION_MODE):
|
||||
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)
|
||||
initial_operation_mode = config.get(CONF_INITIAL_OPERATION_MODE)
|
||||
away_temp = config.get(CONF_AWAY_TEMP)
|
||||
precision = config.get(CONF_PRECISION)
|
||||
|
||||
async_add_entities([GenericThermostat(
|
||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||
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):
|
||||
|
@ -96,7 +102,7 @@ class GenericThermostat(ClimateDevice):
|
|||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
|
||||
cold_tolerance, hot_tolerance, keep_alive,
|
||||
initial_operation_mode, away_temp):
|
||||
initial_operation_mode, away_temp, precision):
|
||||
"""Initialize the thermostat."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
|
@ -109,6 +115,7 @@ class GenericThermostat(ClimateDevice):
|
|||
self._initial_operation_mode = initial_operation_mode
|
||||
self._saved_target_temp = target_temp if target_temp is not None \
|
||||
else away_temp
|
||||
self._temp_precision = precision
|
||||
if self.ac_mode:
|
||||
self._current_operation = STATE_COOL
|
||||
self._operation_list = [STATE_COOL, STATE_OFF]
|
||||
|
@ -202,6 +209,13 @@ class GenericThermostat(ClimateDevice):
|
|||
"""Return the name of the thermostat."""
|
||||
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
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
|
|
|
@ -7,17 +7,16 @@ https://home-assistant.io/components/climate.homematic/
|
|||
import logging
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
|
||||
ClimateDevice)
|
||||
STATE_AUTO, STATE_MANUAL, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE, ClimateDevice)
|
||||
from homeassistant.components.homematic import (
|
||||
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']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_MANUAL = 'manual'
|
||||
STATE_BOOST = 'boost'
|
||||
STATE_COMFORT = 'comfort'
|
||||
STATE_LOWERING = 'lowering'
|
||||
|
@ -41,7 +40,7 @@ HM_HUMI_MAP = [
|
|||
]
|
||||
|
||||
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
|
||||
|
||||
|
@ -78,21 +77,17 @@ class HMThermostat(HMDevice, ClimateDevice):
|
|||
if HM_CONTROL_MODE not in self._data:
|
||||
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
|
||||
if boost_mode:
|
||||
if self._data.get('BOOST_MODE', False):
|
||||
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
|
||||
if not set_point_mode == -1:
|
||||
code = set_point_mode
|
||||
if HMIP_CONTROL_MODE in self._data:
|
||||
code = self._data[HMIP_CONTROL_MODE]
|
||||
# Other devices use the control_mode
|
||||
else:
|
||||
code = control_mode
|
||||
code = self._data['CONTROL_MODE']
|
||||
|
||||
# get the name of the mode
|
||||
name = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][code]
|
||||
|
@ -101,12 +96,15 @@ class HMThermostat(HMDevice, ClimateDevice):
|
|||
@property
|
||||
def operation_list(self):
|
||||
"""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:
|
||||
if mode in HM_STATE_MAP:
|
||||
op_list.append(HM_STATE_MAP.get(mode))
|
||||
|
||||
return op_list
|
||||
|
||||
@property
|
||||
|
@ -157,11 +155,11 @@ class HMThermostat(HMDevice, ClimateDevice):
|
|||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from the Homematic metadata."""
|
||||
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 \
|
||||
HM_IP_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE:
|
||||
self._data[HM_CONTROL_MODE] = STATE_UNKNOWN
|
||||
HMIP_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE:
|
||||
self._data[HM_CONTROL_MODE] = None
|
||||
|
||||
for node in self._hmdevice.SENSORNODE.keys():
|
||||
self._data[node] = STATE_UNKNOWN
|
||||
self._data[node] = None
|
||||
|
|
|
@ -88,6 +88,12 @@ class MelissaClimate(ClimateDevice):
|
|||
if self._data:
|
||||
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
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
|
@ -113,7 +119,8 @@ class MelissaClimate(ClimateDevice):
|
|||
@property
|
||||
def target_temperature(self):
|
||||
"""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]
|
||||
|
||||
@property
|
||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
|||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
REQUIREMENTS = ['millheater==0.2.2']
|
||||
REQUIREMENTS = ['millheater==0.2.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -32,8 +32,7 @@ MIN_TEMP = 5
|
|||
SERVICE_SET_ROOM_TEMP = 'mill_set_room_temperature'
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_FAN_MODE | SUPPORT_ON_OFF |
|
||||
SUPPORT_OPERATION_MODE)
|
||||
SUPPORT_FAN_MODE)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
|
@ -92,12 +91,14 @@ class MillHeater(ClimateDevice):
|
|||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
if self._heater.is_gen1:
|
||||
return SUPPORT_FLAGS
|
||||
return SUPPORT_FLAGS | SUPPORT_ON_OFF | SUPPORT_OPERATION_MODE
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._heater.device_status == 0 # weird api choice
|
||||
return self._heater.available
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
@ -112,16 +113,18 @@ class MillHeater(ClimateDevice):
|
|||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._heater.room:
|
||||
room = self._heater.room.name
|
||||
else:
|
||||
room = "Independent device"
|
||||
return {
|
||||
"room": room,
|
||||
res = {
|
||||
"open_window": self._heater.open_window,
|
||||
"heating": self._heater.is_heating,
|
||||
"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
|
||||
def temperature_unit(self):
|
||||
|
@ -156,6 +159,8 @@ class MillHeater(ClimateDevice):
|
|||
@property
|
||||
def is_on(self):
|
||||
"""Return true if heater is on."""
|
||||
if self._heater.is_gen1:
|
||||
return True
|
||||
return self._heater.power_status == 1
|
||||
|
||||
@property
|
||||
|
@ -176,6 +181,8 @@ class MillHeater(ClimateDevice):
|
|||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
if self._heater.is_gen1:
|
||||
return None
|
||||
return [STATE_HEAT, STATE_OFF]
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
|
@ -210,7 +217,7 @@ class MillHeater(ClimateDevice):
|
|||
"""Set operation mode."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
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()
|
||||
else:
|
||||
_LOGGER.error("Unrecognized operation mode: %s", operation_mode)
|
||||
|
|
|
@ -168,18 +168,14 @@ class NestThermostat(ClimateDevice):
|
|||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._mode != NEST_MODE_HEAT_COOL and \
|
||||
self._mode != STATE_ECO and \
|
||||
not self.is_away_mode_on:
|
||||
if self._mode not in (NEST_MODE_HEAT_COOL, STATE_ECO):
|
||||
return self._target_temperature
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if (self.is_away_mode_on or self._mode == STATE_ECO) and \
|
||||
self._eco_temperature[0]:
|
||||
# eco_temperature is always a low, high tuple
|
||||
if self._mode == STATE_ECO:
|
||||
return self._eco_temperature[0]
|
||||
if self._mode == NEST_MODE_HEAT_COOL:
|
||||
return self._target_temperature[0]
|
||||
|
@ -188,9 +184,7 @@ class NestThermostat(ClimateDevice):
|
|||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
if (self.is_away_mode_on or self._mode == STATE_ECO) and \
|
||||
self._eco_temperature[1]:
|
||||
# eco_temperature is always a low, high tuple
|
||||
if self._mode == STATE_ECO:
|
||||
return self._eco_temperature[1]
|
||||
if self._mode == NEST_MODE_HEAT_COOL:
|
||||
return self._target_temperature[1]
|
||||
|
|
|
@ -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()
|
|
@ -12,24 +12,20 @@ import os
|
|||
import voluptuous as vol
|
||||
|
||||
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.util import dt as dt_util
|
||||
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 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
|
||||
|
||||
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__)
|
||||
_UNDEF = object()
|
||||
|
||||
CONF_ALEXA = 'alexa'
|
||||
CONF_ALIASES = 'aliases'
|
||||
|
@ -68,7 +64,7 @@ ALEXA_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({
|
||||
|
@ -124,12 +120,11 @@ class Cloud:
|
|||
self.alexa_config = alexa
|
||||
self.google_actions_user_conf = google_actions
|
||||
self._gactions_config = None
|
||||
self._prefs = None
|
||||
self.prefs = prefs.CloudPreferences(hass)
|
||||
self.id_token = None
|
||||
self.access_token = None
|
||||
self.refresh_token = None
|
||||
self.iot = iot.CloudIoT(self)
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
if mode == MODE_DEV:
|
||||
self.cognito_client_id = cognito_client_id
|
||||
|
@ -184,26 +179,20 @@ class Cloud:
|
|||
|
||||
def should_expose(entity):
|
||||
"""If an entity should be exposed."""
|
||||
if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
|
||||
return conf['filter'](entity.entity_id)
|
||||
|
||||
self._gactions_config = ga_h.Config(
|
||||
should_expose=should_expose,
|
||||
agent_user_id=self.claims['cognito:username'],
|
||||
entity_config=conf.get(CONF_ENTITY_CONFIG),
|
||||
allow_unlock=self.prefs.google_allow_unlock,
|
||||
)
|
||||
|
||||
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):
|
||||
"""Get config path inside cloud dir.
|
||||
|
||||
|
@ -243,20 +232,6 @@ class Cloud:
|
|||
|
||||
async def async_start(self, _):
|
||||
"""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():
|
||||
"""Load config."""
|
||||
# Ensure config dir exists
|
||||
|
@ -273,6 +248,8 @@ class Cloud:
|
|||
|
||||
info = await self.hass.async_add_job(load_config)
|
||||
|
||||
await self.prefs.async_initialize(bool(info))
|
||||
|
||||
if info is None:
|
||||
return
|
||||
|
||||
|
@ -282,15 +259,6 @@ class Cloud:
|
|||
|
||||
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
|
||||
"""Decode the claims in a token."""
|
||||
from jose import jwt
|
||||
|
|
|
@ -3,6 +3,10 @@ DOMAIN = 'cloud'
|
|||
CONFIG_DIR = '.cloud'
|
||||
REQUEST_TIMEOUT = 10
|
||||
|
||||
PREF_ENABLE_ALEXA = 'alexa_enabled'
|
||||
PREF_ENABLE_GOOGLE = 'google_enabled'
|
||||
PREF_GOOGLE_ALLOW_UNLOCK = 'google_allow_unlock'
|
||||
|
||||
SERVERS = {
|
||||
'production': {
|
||||
'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
|
||||
|
|
|
@ -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 . 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
|
||||
|
||||
_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'
|
||||
SCHEMA_WS_UPDATE_PREFS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_UPDATE_PREFS,
|
||||
vol.Optional('google_enabled'): bool,
|
||||
vol.Optional('alexa_enabled'): bool,
|
||||
vol.Optional(PREF_ENABLE_GOOGLE): 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.pop('id')
|
||||
changes.pop('type')
|
||||
await cloud.update_preferences(**changes)
|
||||
await cloud.prefs.async_update(**changes)
|
||||
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], {'success': True}))
|
||||
|
@ -308,10 +311,9 @@ def _account_data(cloud):
|
|||
'logged_in': True,
|
||||
'email': claims['email'],
|
||||
'cloud': cloud.iot.state,
|
||||
'google_enabled': cloud.google_enabled,
|
||||
'prefs': cloud.prefs.as_dict(),
|
||||
'google_entities': cloud.google_actions_user_conf['filter'].config,
|
||||
'google_domains': list(google_sh.DOMAIN_TO_GOOGLE_TYPES),
|
||||
'alexa_enabled': cloud.alexa_enabled,
|
||||
'alexa_entities': cloud.alexa_config.should_expose.config,
|
||||
'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS),
|
||||
}
|
||||
|
|
|
@ -229,7 +229,7 @@ def async_handle_alexa(hass, cloud, payload):
|
|||
"""Handle an incoming IoT message for Alexa."""
|
||||
result = yield from alexa.async_handle_message(
|
||||
hass, cloud.alexa_config, payload,
|
||||
enabled=cloud.alexa_enabled)
|
||||
enabled=cloud.prefs.alexa_enabled)
|
||||
return result
|
||||
|
||||
|
||||
|
@ -237,7 +237,7 @@ def async_handle_alexa(hass, cloud, payload):
|
|||
@asyncio.coroutine
|
||||
def async_handle_google_actions(hass, cloud, payload):
|
||||
"""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)
|
||||
|
||||
result = yield from ga.async_handle_message(
|
||||
|
|
|
@ -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)
|
|
@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
DOMAIN = 'coinbase'
|
||||
|
||||
CONF_API_SECRET = 'api_secret'
|
||||
CONF_ACCOUNT_CURRENCIES = 'account_balance_currencies'
|
||||
CONF_EXCHANGE_CURRENCIES = 'exchange_rate_currencies'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
|
||||
|
@ -31,6 +32,8 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_API_KEY): 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.All(cv.ensure_list, [cv.string])
|
||||
})
|
||||
|
@ -45,6 +48,7 @@ def setup(hass, config):
|
|||
"""
|
||||
api_key = config[DOMAIN].get(CONF_API_KEY)
|
||||
api_secret = config[DOMAIN].get(CONF_API_SECRET)
|
||||
account_currencies = config[DOMAIN].get(CONF_ACCOUNT_CURRENCIES)
|
||||
exchange_currencies = config[DOMAIN].get(CONF_EXCHANGE_CURRENCIES)
|
||||
|
||||
hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(
|
||||
|
@ -53,7 +57,13 @@ def setup(hass, config):
|
|||
if not hasattr(coinbase_data, 'accounts'):
|
||||
return False
|
||||
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:
|
||||
if currency not in coinbase_data.exchange_rates.rates:
|
||||
_LOGGER.warning("Currency %s not found", currency)
|
||||
|
|
|
@ -5,7 +5,8 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/cover.deconz/
|
||||
"""
|
||||
from homeassistant.components.deconz.const import (
|
||||
COVER_TYPES, 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 (
|
||||
ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP,
|
||||
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.
|
||||
"""
|
||||
gateway = hass.data[DECONZ_DOMAIN]
|
||||
|
||||
@callback
|
||||
def async_add_cover(lights):
|
||||
"""Add cover from deCONZ."""
|
||||
|
@ -36,23 +39,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
for light in lights:
|
||||
if light.type in COVER_TYPES:
|
||||
if light.modelid in ZIGBEE_SPEC:
|
||||
entities.append(DeconzCoverZigbeeSpec(light))
|
||||
entities.append(DeconzCoverZigbeeSpec(light, gateway))
|
||||
else:
|
||||
entities.append(DeconzCover(light))
|
||||
entities.append(DeconzCover(light, gateway))
|
||||
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_add_cover(hass.data[DATA_DECONZ].api.lights.values())
|
||||
async_add_cover(gateway.api.lights.values())
|
||||
|
||||
|
||||
class DeconzCover(CoverDevice):
|
||||
"""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."""
|
||||
self._cover = cover
|
||||
self.gateway = gateway
|
||||
self.unsub_dispatcher = None
|
||||
|
||||
self._features = SUPPORT_OPEN
|
||||
self._features |= SUPPORT_CLOSE
|
||||
self._features |= SUPPORT_STOP
|
||||
|
@ -61,11 +67,14 @@ class DeconzCover(CoverDevice):
|
|||
async def async_added_to_hass(self):
|
||||
"""Subscribe to covers events."""
|
||||
self._cover.register_async_callback(self.async_update_callback)
|
||||
self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
|
||||
self._cover.deconz_id
|
||||
self.gateway.deconz_ids[self.entity_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:
|
||||
"""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 = None
|
||||
|
||||
|
@ -112,7 +121,7 @@ class DeconzCover(CoverDevice):
|
|||
@property
|
||||
def available(self):
|
||||
"""Return True if light is available."""
|
||||
return self._cover.reachable
|
||||
return self.gateway.available and self._cover.reachable
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -150,7 +159,7 @@ class DeconzCover(CoverDevice):
|
|||
self._cover.uniqueid.count(':') != 7):
|
||||
return None
|
||||
serial = self._cover.uniqueid.split('-', 1)[0]
|
||||
bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid
|
||||
bridgeid = self.gateway.api.config.bridgeid
|
||||
return {
|
||||
'connections': {(CONNECTION_ZIGBEE, serial)},
|
||||
'identifiers': {(DECONZ_DOMAIN, serial)},
|
||||
|
|
|
@ -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")
|
|
@ -279,21 +279,19 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
if self._template is not None:
|
||||
payload = self._template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
|
||||
if payload.isnumeric():
|
||||
if 0 <= int(payload) <= 100:
|
||||
percentage_payload = int(payload)
|
||||
else:
|
||||
percentage_payload = self.find_percentage_in_range(
|
||||
float(payload), COVER_PAYLOAD)
|
||||
if 0 <= percentage_payload <= 100:
|
||||
self._position = percentage_payload
|
||||
self._state = self._position == self._position_closed
|
||||
self._state = percentage_payload == DEFAULT_POSITION_CLOSED
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Payload is not integer within range: %s",
|
||||
payload)
|
||||
return
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._get_position_topic:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._get_position_topic,
|
||||
|
@ -374,7 +372,8 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
# Optimistically assume that cover has changed state.
|
||||
self._state = False
|
||||
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()
|
||||
|
||||
async def async_close_cover(self, **kwargs):
|
||||
|
@ -389,7 +388,8 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
# Optimistically assume that cover has changed state.
|
||||
self._state = True
|
||||
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()
|
||||
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
|
@ -469,6 +469,11 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
offset_position = position - min_range
|
||||
position_percentage = round(
|
||||
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:
|
||||
return 100 - position_percentage
|
||||
return position_percentage
|
||||
|
|
|
@ -9,18 +9,15 @@ import logging
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN)
|
||||
PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, CoverDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_CLOSING,
|
||||
STATE_OPEN, STATE_OPENING)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pymyq==0.0.15']
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pymyq==1.0.0']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'myq'
|
||||
|
||||
MYQ_TO_HASS = {
|
||||
'closed': STATE_CLOSED,
|
||||
'closing': STATE_CLOSING,
|
||||
|
@ -28,95 +25,69 @@ MYQ_TO_HASS = {
|
|||
'opening': STATE_OPENING
|
||||
}
|
||||
|
||||
NOTIFICATION_ID = 'myq_notification'
|
||||
NOTIFICATION_TITLE = 'MyQ Cover Setup'
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_TYPE): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the MyQ component."""
|
||||
from pymyq import MyQAPI as pymyq
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the platform."""
|
||||
from pymyq import login
|
||||
from pymyq.errors import MyQError, UnsupportedBrandError
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
brand = config.get(CONF_TYPE)
|
||||
myq = pymyq(username, password, brand)
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
brand = config[CONF_TYPE]
|
||||
|
||||
try:
|
||||
if not myq.is_supported_brand():
|
||||
raise ValueError("Unsupported type. See documentation")
|
||||
myq = await login(username, password, brand, websession)
|
||||
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():
|
||||
raise ValueError("Username or Password is incorrect")
|
||||
|
||||
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
|
||||
devices = await myq.get_devices()
|
||||
async_add_entities([MyQDevice(device) for device in devices], True)
|
||||
|
||||
|
||||
class MyQDevice(CoverDevice):
|
||||
"""Representation of a MyQ cover."""
|
||||
|
||||
def __init__(self, myq, device):
|
||||
def __init__(self, device):
|
||||
"""Initialize with API object, device id."""
|
||||
self.myq = myq
|
||||
self.device_id = device['deviceid']
|
||||
self._name = device['name']
|
||||
self._status = None
|
||||
self._device = device
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Define this cover as a garage door."""
|
||||
return 'garage'
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Poll for state."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the garage door if any."""
|
||||
return self._name if self._name else DEFAULT_NAME
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if cover is closed, else False."""
|
||||
if self._status in [None, False]:
|
||||
return None
|
||||
return MYQ_TO_HASS.get(self._status) == STATE_CLOSED
|
||||
return MYQ_TO_HASS.get(self._device.state) == STATE_CLOSED
|
||||
|
||||
@property
|
||||
def is_closing(self):
|
||||
"""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
|
||||
def is_opening(self):
|
||||
"""Return if the cover is opening or not."""
|
||||
return MYQ_TO_HASS.get(self._status) == 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)
|
||||
return MYQ_TO_HASS.get(self._device.state) == STATE_OPENING
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
@ -126,8 +97,16 @@ class MyQDevice(CoverDevice):
|
|||
@property
|
||||
def unique_id(self):
|
||||
"""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."""
|
||||
self._status = self.myq.get_status(self.device_id)
|
||||
await self._device.update()
|
||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.helpers import discovery
|
|||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['pydaikin==0.6']
|
||||
REQUIREMENTS = ['pydaikin==0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"init": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Port (default value: '80')"
|
||||
"port": "Port"
|
||||
},
|
||||
"title": "Define deCONZ gateway"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
"init": {
|
||||
"data": {
|
||||
"host": "Vert",
|
||||
"port": "Port (standardverdi: '80')"
|
||||
"port": "Port"
|
||||
},
|
||||
"title": "Definer deCONZ-gatewayen"
|
||||
},
|
||||
|
|
|
@ -11,11 +11,10 @@ from homeassistant.const import (
|
|||
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
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
|
||||
from .config_flow import configured_hosts
|
||||
from .const import CONFIG_FILE, DOMAIN, _LOGGER
|
||||
from .const import DEFAULT_PORT, DOMAIN, _LOGGER
|
||||
from .gateway import DeconzGateway
|
||||
|
||||
REQUIREMENTS = ['pydeconz==47']
|
||||
|
@ -27,7 +26,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_API_KEY): 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)
|
||||
|
||||
|
@ -53,11 +52,7 @@ async def async_setup(hass, config):
|
|||
"""
|
||||
if DOMAIN in config:
|
||||
deconz_config = None
|
||||
config_file = await hass.async_add_job(
|
||||
load_json, hass.config.path(CONFIG_FILE))
|
||||
if config_file:
|
||||
deconz_config = config_file
|
||||
elif CONF_HOST in config[DOMAIN]:
|
||||
if CONF_HOST in config[DOMAIN]:
|
||||
deconz_config = config[DOMAIN]
|
||||
if deconz_config and not configured_hosts(hass):
|
||||
hass.async_add_job(hass.config_entries.flow.async_init(
|
||||
|
|
|
@ -6,11 +6,9 @@ from homeassistant import config_entries
|
|||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.util.json import load_json
|
||||
|
||||
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'
|
||||
|
||||
|
@ -35,6 +33,10 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
|
|||
self.deconz_config = {}
|
||||
|
||||
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.
|
||||
|
||||
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]:
|
||||
self.deconz_config = bridge
|
||||
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)
|
||||
self.bridges = await async_discovery(session)
|
||||
|
@ -58,19 +62,24 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
|
|||
if len(self.bridges) == 1:
|
||||
self.deconz_config = self.bridges[0]
|
||||
return await self.async_step_link()
|
||||
|
||||
if len(self.bridges) > 1:
|
||||
hosts = []
|
||||
for bridge in self.bridges:
|
||||
hosts.append(bridge[CONF_HOST])
|
||||
return self.async_show_form(
|
||||
step_id='user',
|
||||
step_id='init',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_HOST): vol.In(hosts)
|
||||
})
|
||||
)
|
||||
|
||||
return self.async_abort(
|
||||
reason='no_bridges'
|
||||
return self.async_show_form(
|
||||
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):
|
||||
|
@ -135,13 +144,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
|
|||
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
|
||||
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)
|
||||
|
||||
async def async_step_import(self, import_config):
|
||||
|
|
|
@ -4,11 +4,8 @@ import logging
|
|||
_LOGGER = logging.getLogger('homeassistant.components.deconz')
|
||||
|
||||
DOMAIN = 'deconz'
|
||||
CONFIG_FILE = 'deconz.conf'
|
||||
DATA_DECONZ_EVENT = 'deconz_events'
|
||||
DATA_DECONZ_ID = 'deconz_entities'
|
||||
DATA_DECONZ_UNSUB = 'deconz_dispatchers'
|
||||
DECONZ_DOMAIN = 'deconz'
|
||||
|
||||
DEFAULT_PORT = 80
|
||||
|
||||
CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor'
|
||||
CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
|
||||
|
@ -16,6 +13,8 @@ CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
|
|||
SUPPORTED_PLATFORMS = ['binary_sensor', 'cover',
|
||||
'light', 'scene', 'sensor', 'switch']
|
||||
|
||||
DECONZ_REACHABLE = 'deconz_reachable'
|
||||
|
||||
ATTR_DARK = 'dark'
|
||||
ATTR_ON = 'on'
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ from homeassistant.helpers.dispatcher import (
|
|||
from homeassistant.util import slugify
|
||||
|
||||
from .const import (
|
||||
_LOGGER, CONF_ALLOW_CLIP_SENSOR, SUPPORTED_PLATFORMS)
|
||||
_LOGGER, DECONZ_REACHABLE, CONF_ALLOW_CLIP_SENSOR, SUPPORTED_PLATFORMS)
|
||||
|
||||
|
||||
class DeconzGateway:
|
||||
|
@ -18,6 +18,7 @@ class DeconzGateway:
|
|||
"""Initialize the system."""
|
||||
self.hass = hass
|
||||
self.config_entry = config_entry
|
||||
self.available = True
|
||||
self.api = None
|
||||
self._cancel_retry_setup = None
|
||||
|
||||
|
@ -30,7 +31,8 @@ class DeconzGateway:
|
|||
hass = self.hass
|
||||
|
||||
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:
|
||||
|
@ -65,6 +67,13 @@ class DeconzGateway:
|
|||
|
||||
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
|
||||
def async_add_device_callback(self, device_type, device):
|
||||
"""Handle event of new device creation in deCONZ."""
|
||||
|
@ -122,13 +131,15 @@ class DeconzGateway:
|
|||
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."""
|
||||
from pydeconz import DeconzSession
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
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()
|
||||
|
||||
if result:
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"title": "Define deCONZ gateway",
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Port (default value: '80')"
|
||||
"port": "Port"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
|
|
|
@ -182,6 +182,9 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
|||
setup = await hass.async_add_job(
|
||||
platform.setup_scanner, hass, p_config, tracker.see,
|
||||
disc_info)
|
||||
elif hasattr(platform, 'async_setup_entry'):
|
||||
setup = await platform.async_setup_entry(
|
||||
hass, p_config, tracker.async_see)
|
||||
else:
|
||||
raise HomeAssistantError("Invalid device_tracker platform.")
|
||||
|
||||
|
@ -197,6 +200,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
|||
except Exception: # pylint: disable=broad-except
|
||||
_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
|
||||
in config_per_platform(config, DOMAIN)]
|
||||
if setup_tasks:
|
||||
|
@ -230,6 +235,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
|||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up an entry."""
|
||||
await hass.data[DOMAIN](entry.domain, entry)
|
||||
return True
|
||||
|
||||
|
||||
class DeviceTracker:
|
||||
"""Representation of a device tracker."""
|
||||
|
||||
|
@ -373,6 +384,7 @@ class DeviceTracker:
|
|||
for device in self.devices.values():
|
||||
if (device.track and device.last_update_home) and \
|
||||
device.stale(now):
|
||||
device.mark_stale()
|
||||
self.hass.async_create_task(device.async_update_ha_state(True))
|
||||
|
||||
async def async_setup_tracked_device(self):
|
||||
|
@ -528,9 +540,15 @@ class Device(Entity):
|
|||
|
||||
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
|
||||
|
||||
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):
|
||||
"""Update state of entity.
|
||||
|
||||
|
@ -550,9 +568,7 @@ class Device(Entity):
|
|||
else:
|
||||
self._state = zone_state.name
|
||||
elif self.stale():
|
||||
self._state = STATE_NOT_HOME
|
||||
self.gps = None
|
||||
self.last_update_home = False
|
||||
self.mark_stale()
|
||||
else:
|
||||
self._state = STATE_HOME
|
||||
self.last_update_home = True
|
||||
|
@ -563,6 +579,7 @@ class Device(Entity):
|
|||
if not state:
|
||||
return
|
||||
self._state = state.state
|
||||
self.last_update_home = (state.state == STATE_HOME)
|
||||
|
||||
for attr, var in (
|
||||
(ATTR_SOURCE_TYPE, 'source_type'),
|
||||
|
|
|
@ -6,43 +6,17 @@ https://home-assistant.io/components/device_tracker.asuswrt/
|
|||
"""
|
||||
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
|
||||
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']
|
||||
DEPENDENCIES = ['asuswrt']
|
||||
|
||||
_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):
|
||||
"""Validate the configuration and return an ASUS-WRT scanner."""
|
||||
scanner = AsusWrtDeviceScanner(config[DOMAIN])
|
||||
scanner = AsusWrtDeviceScanner(hass.data[DATA_ASUSWRT])
|
||||
await scanner.async_connect()
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
@ -51,19 +25,11 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
|||
"""This class queries a router running ASUSWRT firmware."""
|
||||
|
||||
# Eighth attribute needed for mode (AP mode vs router mode)
|
||||
def __init__(self, config):
|
||||
def __init__(self, api):
|
||||
"""Initialize the scanner."""
|
||||
from aioasuswrt.asuswrt import AsusWrt
|
||||
|
||||
self.last_results = {}
|
||||
self.success_init = False
|
||||
self.connection = AsusWrt(config[CONF_HOST], config[CONF_PORT],
|
||||
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])
|
||||
self.connection = api
|
||||
|
||||
async def async_connect(self):
|
||||
"""Initialize connection to the router."""
|
||||
|
|
|
@ -4,129 +4,26 @@ Support for the Geofency platform.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.geofency/
|
||||
"""
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE, ATTR_LONGITUDE, HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.components.geofency import TRACKER_UPDATE
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
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]),
|
||||
})
|
||||
DEPENDENCIES = ['geofency']
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Set up an endpoint for the Geofency application."""
|
||||
mobile_beacons = config.get(CONF_MOBILE_BEACONS) or []
|
||||
|
||||
hass.http.register_view(GeofencyView(see, mobile_beacons))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class GeofencyView(HomeAssistantView):
|
||||
"""View to handle Geofency requests."""
|
||||
|
||||
url = URL
|
||||
name = 'api:geofency'
|
||||
|
||||
def __init__(self, see, mobile_beacons):
|
||||
"""Initialize Geofency url endpoints."""
|
||||
self.see = see
|
||||
self.mobile_beacons = [slugify(beacon) for beacon in mobile_beacons]
|
||||
|
||||
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):
|
||||
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Set up the Geofency device tracker."""
|
||||
async def _set_location(device, gps, location_name, attributes):
|
||||
"""Fire HA event to set location."""
|
||||
device = self._device_name(data)
|
||||
|
||||
await hass.async_add_job(
|
||||
partial(self.see, dev_id=device,
|
||||
gps=(data[ATTR_LATITUDE], data[ATTR_LONGITUDE]),
|
||||
await async_see(
|
||||
dev_id=device,
|
||||
gps=gps,
|
||||
location_name=location_name,
|
||||
attributes=data))
|
||||
attributes=attributes
|
||||
)
|
||||
|
||||
return "Setting location for {}".format(device)
|
||||
async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location)
|
||||
return True
|
||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.helpers.event import track_time_interval
|
|||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import slugify, dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['locationsharinglib==3.0.7']
|
||||
REQUIREMENTS = ['locationsharinglib==3.0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -7,6 +7,7 @@ https://home-assistant.io/components/device_tracker.luci/
|
|||
import json
|
||||
import logging
|
||||
import re
|
||||
from collections import namedtuple
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
@ -43,14 +44,17 @@ def get_scanner(hass, config):
|
|||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
Device = namedtuple('Device', ['mac', 'ip', 'flags', 'device', 'host'])
|
||||
|
||||
|
||||
class LuciDeviceScanner(DeviceScanner):
|
||||
"""This class queries a wireless router running OpenWrt firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
host = config[CONF_HOST]
|
||||
self.host = config[CONF_HOST]
|
||||
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.password = config[CONF_PASSWORD]
|
||||
|
||||
|
@ -68,7 +72,7 @@ class LuciDeviceScanner(DeviceScanner):
|
|||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return self.last_results
|
||||
return [device.mac for device in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
|
@ -88,6 +92,18 @@ class LuciDeviceScanner(DeviceScanner):
|
|||
return
|
||||
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):
|
||||
"""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
|
||||
# NUD_REACHABLE and if so, add it to last_results
|
||||
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
|
||||
|
||||
|
|
|
@ -6,25 +6,30 @@ https://home-assistant.io/components/device_tracker.mikrotik/
|
|||
"""
|
||||
import logging
|
||||
|
||||
import ssl
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL, CONF_METHOD)
|
||||
|
||||
REQUIREMENTS = ['librouteros==2.1.1']
|
||||
|
||||
MTK_DEFAULT_API_PORT = '8728'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MTK_DEFAULT_API_PORT = '8728'
|
||||
MTK_DEFAULT_API_SSL_PORT = '8729'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): 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.host = config[CONF_HOST]
|
||||
self.ssl = config[CONF_SSL]
|
||||
try:
|
||||
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.password = config[CONF_PASSWORD]
|
||||
self.method = config.get(CONF_METHOD)
|
||||
|
||||
self.connected = False
|
||||
self.success_init = False
|
||||
|
@ -53,27 +66,29 @@ class MikrotikScanner(DeviceScanner):
|
|||
self.success_init = self.connect_to_device()
|
||||
|
||||
if self.success_init:
|
||||
_LOGGER.info(
|
||||
"Start polling Mikrotik (%s) router...",
|
||||
self.host
|
||||
)
|
||||
_LOGGER.info("Start polling Mikrotik (%s) router...", self.host)
|
||||
self._update_info()
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Connection to Mikrotik (%s) failed",
|
||||
self.host
|
||||
)
|
||||
_LOGGER.error("Connection to Mikrotik (%s) failed", self.host)
|
||||
|
||||
def connect_to_device(self):
|
||||
"""Connect to Mikrotik method."""
|
||||
import librouteros
|
||||
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.host,
|
||||
self.username,
|
||||
self.password,
|
||||
port=int(self.port),
|
||||
encoding='utf-8'
|
||||
**kwargs
|
||||
)
|
||||
|
||||
try:
|
||||
|
@ -86,16 +101,15 @@ class MikrotikScanner(DeviceScanner):
|
|||
raise
|
||||
|
||||
if routerboard_info:
|
||||
_LOGGER.info("Connected to Mikrotik %s with IP %s",
|
||||
routerboard_info[0].get('model', 'Router'),
|
||||
self.host)
|
||||
_LOGGER.info(
|
||||
"Connected to Mikrotik %s with IP %s",
|
||||
routerboard_info[0].get('model', 'Router'), self.host)
|
||||
|
||||
self.connected = True
|
||||
|
||||
try:
|
||||
self.capsman_exist = self.client(
|
||||
cmd='/caps-man/interface/getall'
|
||||
)
|
||||
cmd='/caps-man/interface/getall')
|
||||
except (librouteros.exceptions.TrapError,
|
||||
librouteros.exceptions.MultiTrapError,
|
||||
librouteros.exceptions.ConnectionError):
|
||||
|
@ -103,27 +117,27 @@ class MikrotikScanner(DeviceScanner):
|
|||
|
||||
if not self.capsman_exist:
|
||||
_LOGGER.info(
|
||||
'Mikrotik %s: Not a CAPSman controller. Trying '
|
||||
'local interfaces ',
|
||||
self.host
|
||||
)
|
||||
"Mikrotik %s: Not a CAPSman controller. Trying "
|
||||
"local interfaces", self.host)
|
||||
|
||||
try:
|
||||
self.wireless_exist = self.client(
|
||||
cmd='/interface/wireless/getall'
|
||||
)
|
||||
cmd='/interface/wireless/getall')
|
||||
except (librouteros.exceptions.TrapError,
|
||||
librouteros.exceptions.MultiTrapError,
|
||||
librouteros.exceptions.ConnectionError):
|
||||
self.wireless_exist = False
|
||||
|
||||
if not self.wireless_exist:
|
||||
if not self.wireless_exist or self.method == 'ip':
|
||||
_LOGGER.info(
|
||||
'Mikrotik %s: Wireless adapters not found. Try to '
|
||||
'use DHCP lease table as presence tracker source. '
|
||||
'Please decrease lease time as much as possible.',
|
||||
self.host
|
||||
)
|
||||
"Mikrotik %s: Wireless adapters not found. Try to "
|
||||
"use DHCP lease table as presence tracker source. "
|
||||
"Please decrease lease time as much as possible",
|
||||
self.host)
|
||||
if self.method:
|
||||
_LOGGER.info(
|
||||
"Mikrotik %s: Manually selected polling method %s",
|
||||
self.host, self.method)
|
||||
|
||||
except (librouteros.exceptions.TrapError,
|
||||
librouteros.exceptions.MultiTrapError,
|
||||
|
@ -143,6 +157,9 @@ class MikrotikScanner(DeviceScanner):
|
|||
|
||||
def _update_info(self):
|
||||
"""Retrieve latest information from the Mikrotik box."""
|
||||
if self.method:
|
||||
devices_tracker = self.method
|
||||
else:
|
||||
if self.capsman_exist:
|
||||
devices_tracker = 'capsman'
|
||||
elif self.wireless_exist:
|
||||
|
@ -152,19 +169,15 @@ class MikrotikScanner(DeviceScanner):
|
|||
|
||||
_LOGGER.info(
|
||||
"Loading %s devices from Mikrotik (%s) ...",
|
||||
devices_tracker,
|
||||
self.host
|
||||
)
|
||||
devices_tracker, self.host)
|
||||
|
||||
device_names = self.client(cmd='/ip/dhcp-server/lease/getall')
|
||||
if devices_tracker == 'capsman':
|
||||
devices = self.client(
|
||||
cmd='/caps-man/registration-table/getall'
|
||||
)
|
||||
cmd='/caps-man/registration-table/getall')
|
||||
elif devices_tracker == 'wireless':
|
||||
devices = self.client(
|
||||
cmd='/interface/wireless/registration-table/getall'
|
||||
)
|
||||
cmd='/interface/wireless/registration-table/getall')
|
||||
else:
|
||||
devices = device_names
|
||||
|
||||
|
@ -172,21 +185,17 @@ class MikrotikScanner(DeviceScanner):
|
|||
return False
|
||||
|
||||
mac_names = {device.get('mac-address'): device.get('host-name')
|
||||
for device in device_names
|
||||
if device.get('mac-address')}
|
||||
for device in device_names if device.get('mac-address')}
|
||||
|
||||
if self.wireless_exist or self.capsman_exist:
|
||||
if devices_tracker in ('wireless', 'capsman'):
|
||||
self.last_results = {
|
||||
device.get('mac-address'):
|
||||
mac_names.get(device.get('mac-address'))
|
||||
for device in devices
|
||||
}
|
||||
for device in devices}
|
||||
else:
|
||||
self.last_results = {
|
||||
device.get('mac-address'):
|
||||
mac_names.get(device.get('mac-address'))
|
||||
for device in device_names
|
||||
if device.get('active-address')
|
||||
}
|
||||
for device in device_names if device.get('active-address')}
|
||||
|
||||
return True
|
||||
|
|
|
@ -19,11 +19,16 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
|||
return False
|
||||
|
||||
for device in new_devices:
|
||||
gateway_id = id(device.gateway)
|
||||
dev_id = (
|
||||
id(device.gateway), device.node_id, device.child_id,
|
||||
gateway_id, device.node_id, device.child_id,
|
||||
device.value_type)
|
||||
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)
|
||||
|
||||
return True
|
||||
|
|
|
@ -7,55 +7,29 @@ https://home-assistant.io/components/device_tracker.owntracks/
|
|||
import base64
|
||||
import json
|
||||
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.device_tracker import (
|
||||
PLATFORM_SCHEMA, ATTR_SOURCE_TYPE, SOURCE_TYPE_BLUETOOTH_LE,
|
||||
SOURCE_TYPE_GPS
|
||||
ATTR_SOURCE_TYPE, SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS
|
||||
)
|
||||
from homeassistant.components.owntracks import DOMAIN as OT_DOMAIN
|
||||
from homeassistant.const import STATE_HOME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.util import slugify, decorator
|
||||
|
||||
REQUIREMENTS = ['libnacl==1.6.1']
|
||||
|
||||
DEPENDENCIES = ['owntracks']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HANDLERS = decorator.Registry()
|
||||
|
||||
BEACON_DEV_ID = 'beacon'
|
||||
|
||||
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
|
||||
CONF_SECRET = 'secret'
|
||||
CONF_WAYPOINT_IMPORT = 'waypoints'
|
||||
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
|
||||
CONF_MQTT_TOPIC = 'mqtt_topic'
|
||||
CONF_REGION_MAPPING = 'region_mapping'
|
||||
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
|
||||
})
|
||||
async def async_setup_entry(hass, entry, async_see):
|
||||
"""Set up OwnTracks based off an entry."""
|
||||
hass.data[OT_DOMAIN]['context'].async_see = async_see
|
||||
hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
OT_DOMAIN, async_handle_message)
|
||||
return True
|
||||
|
||||
|
||||
def get_cipher():
|
||||
|
@ -72,29 +46,6 @@ def get_cipher():
|
|||
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):
|
||||
"""Parse an MQTT topic {sub_topic}/user/dev, return (user, dev) tuple.
|
||||
|
||||
|
@ -202,93 +153,6 @@ def _decrypt_payload(secret, topic, ciphertext):
|
|||
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')
|
||||
async def async_handle_location_message(hass, context, message):
|
||||
"""Handle a location message."""
|
||||
|
@ -485,6 +349,8 @@ async def async_handle_message(hass, context, message):
|
|||
"""Handle an OwnTracks message."""
|
||||
msgtype = message.get('_type')
|
||||
|
||||
_LOGGER.debug("Received %s", message)
|
||||
|
||||
handler = HANDLERS.get(msgtype, async_handle_unsupported_msg)
|
||||
|
||||
await handler(hass, context, message)
|
||||
|
|
|
@ -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
|
|
@ -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})')
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string
|
||||
vol.Optional(CONF_HOST): cv.string
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.util import slugify
|
|||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pytile==2.0.2']
|
||||
REQUIREMENTS = ['pytile==2.0.5']
|
||||
|
||||
CLIENT_UUID_CONFIG_FILE = '.tile.conf'
|
||||
DEVICE_TYPES = ['PHONE', 'TILE']
|
||||
|
|
|
@ -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)
|
|
@ -61,7 +61,8 @@ class XiaomiMiioDeviceScanner(DeviceScanner):
|
|||
|
||||
devices = []
|
||||
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)
|
||||
|
||||
for device in station_info.associated_stations:
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Dialogflow Webhook einrichten"
|
||||
}
|
||||
},
|
||||
"title": "Dialogflow"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -76,7 +76,7 @@ async def handle_webhook(hass, webhook_id, request):
|
|||
async def async_setup_entry(hass, entry):
|
||||
"""Configure based on config entry."""
|
||||
hass.components.webhook.async_register(
|
||||
entry.data[CONF_WEBHOOK_ID], handle_webhook)
|
||||
DOMAIN, 'DialogFlow', entry.data[CONF_WEBHOOK_ID], handle_webhook)
|
||||
return True
|
||||
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue