2019-04-03 15:40:03 +00:00
|
|
|
"""Nuki.io lock platform."""
|
2017-02-02 14:15:27 +00:00
|
|
|
from datetime import timedelta
|
|
|
|
import logging
|
2019-07-16 15:06:47 +00:00
|
|
|
import requests
|
2017-05-02 16:18:47 +00:00
|
|
|
|
2017-02-02 14:15:27 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2019-07-16 15:06:47 +00:00
|
|
|
from homeassistant.components.lock import (
|
2019-07-31 19:25:30 +00:00
|
|
|
DOMAIN,
|
|
|
|
PLATFORM_SCHEMA,
|
|
|
|
LockDevice,
|
|
|
|
SUPPORT_OPEN,
|
|
|
|
)
|
|
|
|
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, CONF_TOKEN
|
2018-01-21 06:35:38 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2017-08-07 12:58:31 +00:00
|
|
|
from homeassistant.helpers.service import extract_entity_ids
|
2017-02-02 14:15:27 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
DEFAULT_PORT = 8080
|
2019-07-16 15:06:47 +00:00
|
|
|
DEFAULT_TIMEOUT = 20
|
2017-02-02 14:15:27 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_BATTERY_CRITICAL = "battery_critical"
|
|
|
|
ATTR_NUKI_ID = "nuki_id"
|
|
|
|
ATTR_UNLATCH = "unlatch"
|
2018-01-21 06:35:38 +00:00
|
|
|
|
|
|
|
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=5)
|
|
|
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
NUKI_DATA = "nuki"
|
2018-01-21 06:35:38 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
SERVICE_LOCK_N_GO = "lock_n_go"
|
2019-07-16 15:06:47 +00:00
|
|
|
SERVICE_CHECK_CONNECTION = "check_connection"
|
2017-08-07 12:58:31 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_HOST): cv.string,
|
|
|
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
|
|
|
vol.Required(CONF_TOKEN): cv.string,
|
|
|
|
}
|
|
|
|
)
|
2017-02-02 14:15:27 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
LOCK_N_GO_SERVICE_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
|
|
|
vol.Optional(ATTR_UNLATCH, default=False): cv.boolean,
|
|
|
|
}
|
|
|
|
)
|
2017-08-07 12:58:31 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
CHECK_CONNECTION_SERVICE_SCHEMA = vol.Schema(
|
|
|
|
{vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}
|
|
|
|
)
|
2017-02-02 14:15:27 +00:00
|
|
|
|
|
|
|
|
2018-08-24 14:37:30 +00:00
|
|
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Set up the Nuki lock platform."""
|
2017-02-02 14:15:27 +00:00
|
|
|
from pynuki import NukiBridge
|
2019-07-31 19:25:30 +00:00
|
|
|
|
|
|
|
bridge = NukiBridge(
|
|
|
|
config[CONF_HOST], config[CONF_TOKEN], config[CONF_PORT], DEFAULT_TIMEOUT
|
|
|
|
)
|
|
|
|
add_entities([NukiLock(lock) for lock in bridge.locks])
|
2017-02-02 14:15:27 +00:00
|
|
|
|
2017-08-07 12:58:31 +00:00
|
|
|
def service_handler(service):
|
|
|
|
"""Service handler for nuki services."""
|
|
|
|
entity_ids = extract_entity_ids(hass, service)
|
|
|
|
all_locks = hass.data[NUKI_DATA][DOMAIN]
|
|
|
|
target_locks = []
|
|
|
|
if not entity_ids:
|
|
|
|
target_locks = all_locks
|
|
|
|
else:
|
|
|
|
for lock in all_locks:
|
|
|
|
if lock.entity_id in entity_ids:
|
|
|
|
target_locks.append(lock)
|
|
|
|
for lock in target_locks:
|
|
|
|
if service.service == SERVICE_LOCK_N_GO:
|
|
|
|
unlatch = service.data[ATTR_UNLATCH]
|
|
|
|
lock.lock_n_go(unlatch=unlatch)
|
2019-07-16 15:06:47 +00:00
|
|
|
elif service.service == SERVICE_CHECK_CONNECTION:
|
|
|
|
lock.check_connection()
|
2017-08-07 12:58:31 +00:00
|
|
|
|
|
|
|
hass.services.register(
|
2019-07-31 19:25:30 +00:00
|
|
|
"nuki", SERVICE_LOCK_N_GO, service_handler, schema=LOCK_N_GO_SERVICE_SCHEMA
|
|
|
|
)
|
2017-08-07 12:58:31 +00:00
|
|
|
hass.services.register(
|
2019-07-31 19:25:30 +00:00
|
|
|
"nuki",
|
|
|
|
SERVICE_CHECK_CONNECTION,
|
|
|
|
service_handler,
|
|
|
|
schema=CHECK_CONNECTION_SERVICE_SCHEMA,
|
|
|
|
)
|
2017-08-07 12:58:31 +00:00
|
|
|
|
2017-02-02 14:15:27 +00:00
|
|
|
|
|
|
|
class NukiLock(LockDevice):
|
|
|
|
"""Representation of a Nuki lock."""
|
|
|
|
|
|
|
|
def __init__(self, nuki_lock):
|
|
|
|
"""Initialize the lock."""
|
|
|
|
self._nuki_lock = nuki_lock
|
|
|
|
self._locked = nuki_lock.is_locked
|
|
|
|
self._name = nuki_lock.name
|
2017-08-07 12:58:31 +00:00
|
|
|
self._battery_critical = nuki_lock.battery_critical
|
2019-07-16 15:06:47 +00:00
|
|
|
self._available = nuki_lock.state != 255
|
2017-08-07 12:58:31 +00:00
|
|
|
|
2018-10-01 06:56:50 +00:00
|
|
|
async def async_added_to_hass(self):
|
2018-01-21 06:35:38 +00:00
|
|
|
"""Call when entity is added to hass."""
|
2017-08-07 12:58:31 +00:00
|
|
|
if NUKI_DATA not in self.hass.data:
|
|
|
|
self.hass.data[NUKI_DATA] = {}
|
|
|
|
if DOMAIN not in self.hass.data[NUKI_DATA]:
|
|
|
|
self.hass.data[NUKI_DATA][DOMAIN] = []
|
|
|
|
self.hass.data[NUKI_DATA][DOMAIN].append(self)
|
2017-02-02 14:15:27 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the name of the lock."""
|
|
|
|
return self._name
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_locked(self):
|
|
|
|
"""Return true if lock is locked."""
|
|
|
|
return self._locked
|
|
|
|
|
2017-08-07 12:58:31 +00:00
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
"""Return the device specific state attributes."""
|
|
|
|
data = {
|
|
|
|
ATTR_BATTERY_CRITICAL: self._battery_critical,
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_NUKI_ID: self._nuki_lock.nuki_id,
|
|
|
|
}
|
2017-08-07 12:58:31 +00:00
|
|
|
return data
|
|
|
|
|
2019-07-16 15:06:47 +00:00
|
|
|
@property
|
|
|
|
def supported_features(self):
|
|
|
|
"""Flag supported features."""
|
|
|
|
return SUPPORT_OPEN
|
|
|
|
|
|
|
|
@property
|
|
|
|
def available(self) -> bool:
|
|
|
|
"""Return True if entity is available."""
|
|
|
|
return self._available
|
|
|
|
|
2017-02-02 14:15:27 +00:00
|
|
|
def update(self):
|
|
|
|
"""Update the nuki lock properties."""
|
2019-07-16 15:06:47 +00:00
|
|
|
try:
|
|
|
|
self._nuki_lock.update(aggressive=False)
|
|
|
|
except requests.exceptions.RequestException:
|
|
|
|
self._available = False
|
2019-08-12 03:48:56 +00:00
|
|
|
return
|
|
|
|
|
2019-09-10 21:23:27 +00:00
|
|
|
self._available = True
|
2019-08-12 03:48:56 +00:00
|
|
|
self._name = self._nuki_lock.name
|
|
|
|
self._locked = self._nuki_lock.is_locked
|
|
|
|
self._battery_critical = self._nuki_lock.battery_critical
|
2017-02-02 14:15:27 +00:00
|
|
|
|
|
|
|
def lock(self, **kwargs):
|
|
|
|
"""Lock the device."""
|
|
|
|
self._nuki_lock.lock()
|
|
|
|
|
|
|
|
def unlock(self, **kwargs):
|
|
|
|
"""Unlock the device."""
|
|
|
|
self._nuki_lock.unlock()
|
2017-08-07 12:58:31 +00:00
|
|
|
|
2019-07-16 15:06:47 +00:00
|
|
|
def open(self, **kwargs):
|
|
|
|
"""Open the door latch."""
|
|
|
|
self._nuki_lock.unlatch()
|
|
|
|
|
2017-08-07 12:58:31 +00:00
|
|
|
def lock_n_go(self, unlatch=False, **kwargs):
|
|
|
|
"""Lock and go.
|
|
|
|
|
|
|
|
This will first unlock the door, then wait for 20 seconds (or another
|
|
|
|
amount of time depending on the lock settings) and relock.
|
|
|
|
"""
|
|
|
|
self._nuki_lock.lock_n_go(unlatch, kwargs)
|
|
|
|
|
2019-07-16 15:06:47 +00:00
|
|
|
def check_connection(self, **kwargs):
|
|
|
|
"""Update the nuki lock properties."""
|
|
|
|
try:
|
|
|
|
self._nuki_lock.update(aggressive=True)
|
|
|
|
except requests.exceptions.RequestException:
|
|
|
|
self._available = False
|
|
|
|
else:
|
|
|
|
self._available = self._nuki_lock.state != 255
|