Merge branch 'dev' into rc
This commit is contained in:
commit
a566804f7f
194 changed files with 7617 additions and 2176 deletions
16
.coveragerc
16
.coveragerc
|
@ -94,6 +94,12 @@ omit =
|
|||
homeassistant/components/envisalink.py
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/fritzbox.py
|
||||
homeassistant/components/*/fritzbox.py
|
||||
|
||||
homeassistant/components/eufy.py
|
||||
homeassistant/components/*/eufy.py
|
||||
|
||||
homeassistant/components/gc100.py
|
||||
homeassistant/components/*/gc100.py
|
||||
|
||||
|
@ -106,6 +112,9 @@ omit =
|
|||
homeassistant/components/hive.py
|
||||
homeassistant/components/*/hive.py
|
||||
|
||||
homeassistant/components/homekit_controller/__init__.py
|
||||
homeassistant/components/*/homekit_controller.py
|
||||
|
||||
homeassistant/components/homematic/__init__.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
|
@ -190,8 +199,8 @@ omit =
|
|||
homeassistant/components/pilight.py
|
||||
homeassistant/components/*/pilight.py
|
||||
|
||||
homeassistant/components/qwikswitch.py
|
||||
homeassistant/components/*/qwikswitch.py
|
||||
homeassistant/components/switch/qwikswitch.py
|
||||
homeassistant/components/light/qwikswitch.py
|
||||
|
||||
homeassistant/components/rachio.py
|
||||
homeassistant/components/*/rachio.py
|
||||
|
@ -639,7 +648,9 @@ omit =
|
|||
homeassistant/components/sensor/sensehat.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/serial.py
|
||||
homeassistant/components/sensor/sht31.py
|
||||
homeassistant/components/sensor/shodan.py
|
||||
homeassistant/components/sensor/sigfox.py
|
||||
homeassistant/components/sensor/simulated.py
|
||||
homeassistant/components/sensor/skybeacon.py
|
||||
homeassistant/components/sensor/sma.py
|
||||
|
@ -669,6 +680,7 @@ omit =
|
|||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/upnp.py
|
||||
homeassistant/components/sensor/ups.py
|
||||
homeassistant/components/sensor/uscis.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/viaggiatreno.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
|
|
|
@ -31,7 +31,7 @@ script: travis_wait 30 tox --develop
|
|||
services:
|
||||
- docker
|
||||
before_deploy:
|
||||
- docker pull lokalise/lokalise-cli@sha256:79b3108211ed1fcc9f7b09a011bfc53c240fc2f3b7fa7f0c8390f593271b4cd7
|
||||
- docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21
|
||||
deploy:
|
||||
skip_cleanup: true
|
||||
provider: script
|
||||
|
|
|
@ -63,6 +63,7 @@ homeassistant/components/media_player/xiaomi_tv.py @fattdev
|
|||
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
|
||||
homeassistant/components/plant.py @ChristianKuehnel
|
||||
homeassistant/components/sensor/airvisual.py @bachya
|
||||
homeassistant/components/sensor/filter.py @dgomes
|
||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
|
@ -72,6 +73,7 @@ homeassistant/components/sensor/sma.py @kellerza
|
|||
homeassistant/components/sensor/sql.py @dgomes
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
||||
homeassistant/components/sensor/upnp.py @dgomes
|
||||
homeassistant/components/sensor/waqi.py @andrey-git
|
||||
homeassistant/components/switch/rainmachine.py @bachya
|
||||
homeassistant/components/switch/tplink.py @rytilahti
|
||||
|
|
|
@ -126,6 +126,10 @@ def get_arguments() -> argparse.Namespace:
|
|||
default=None,
|
||||
help='Log file to write to. If not set, CONFIG/home-assistant.log '
|
||||
'is used')
|
||||
parser.add_argument(
|
||||
'--log-no-color',
|
||||
action='store_true',
|
||||
help="Disable color logs")
|
||||
parser.add_argument(
|
||||
'--runner',
|
||||
action='store_true',
|
||||
|
@ -259,13 +263,14 @@ def setup_and_run_hass(config_dir: str,
|
|||
hass = bootstrap.from_config_dict(
|
||||
config, config_dir=config_dir, verbose=args.verbose,
|
||||
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
|
||||
log_file=args.log_file)
|
||||
log_file=args.log_file, log_no_color=args.log_no_color)
|
||||
else:
|
||||
config_file = ensure_config_file(config_dir)
|
||||
print('Config directory:', config_dir)
|
||||
hass = bootstrap.from_config_file(
|
||||
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
|
||||
log_rotate_days=args.log_rotate_days, log_file=args.log_file)
|
||||
log_rotate_days=args.log_rotate_days, log_file=args.log_file,
|
||||
log_no_color=args.log_no_color)
|
||||
|
||||
if hass is None:
|
||||
return None
|
||||
|
|
|
@ -42,7 +42,8 @@ def from_config_dict(config: Dict[str, Any],
|
|||
verbose: bool = False,
|
||||
skip_pip: bool = False,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None) \
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
|
@ -60,7 +61,7 @@ def from_config_dict(config: Dict[str, Any],
|
|||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_dict(
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days, log_file)
|
||||
log_rotate_days, log_file, log_no_color)
|
||||
)
|
||||
|
||||
return hass
|
||||
|
@ -74,7 +75,8 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||
verbose: bool = False,
|
||||
skip_pip: bool = False,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None) \
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
|
@ -84,7 +86,8 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||
start = time()
|
||||
|
||||
if enable_log:
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file)
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
||||
log_no_color)
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
|
@ -164,7 +167,8 @@ def from_config_file(config_path: str,
|
|||
verbose: bool = False,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None):
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False):
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter if given,
|
||||
|
@ -176,7 +180,8 @@ def from_config_file(config_path: str,
|
|||
# run task
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_file(
|
||||
config_path, hass, verbose, skip_pip, log_rotate_days, log_file)
|
||||
config_path, hass, verbose, skip_pip,
|
||||
log_rotate_days, log_file, log_no_color)
|
||||
)
|
||||
|
||||
return hass
|
||||
|
@ -188,7 +193,8 @@ def async_from_config_file(config_path: str,
|
|||
verbose: bool = False,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None):
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False):
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter.
|
||||
|
@ -199,7 +205,8 @@ def async_from_config_file(config_path: str,
|
|||
hass.config.config_dir = config_dir
|
||||
yield from async_mount_local_lib_path(config_dir, hass.loop)
|
||||
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file)
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
||||
log_no_color)
|
||||
|
||||
try:
|
||||
config_dict = yield from hass.async_add_job(
|
||||
|
@ -216,40 +223,51 @@ def async_from_config_file(config_path: str,
|
|||
|
||||
|
||||
@core.callback
|
||||
def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False,
|
||||
log_rotate_days=None, log_file=None) -> None:
|
||||
def async_enable_logging(hass: core.HomeAssistant,
|
||||
verbose: bool = False,
|
||||
log_rotate_days=None,
|
||||
log_file=None,
|
||||
log_no_color: bool = False) -> None:
|
||||
"""Set up the logging.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
|
||||
"[%(name)s] %(message)s")
|
||||
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
|
||||
datefmt = '%Y-%m-%d %H:%M:%S'
|
||||
|
||||
if not log_no_color:
|
||||
try:
|
||||
from colorlog import ColoredFormatter
|
||||
# basicConfig must be called after importing colorlog in order to
|
||||
# ensure that the handlers it sets up wraps the correct streams.
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
|
||||
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
|
||||
colorfmt,
|
||||
datefmt=datefmt,
|
||||
reset=True,
|
||||
log_colors={
|
||||
'DEBUG': 'cyan',
|
||||
'INFO': 'green',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'red',
|
||||
}
|
||||
))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# If the above initialization failed for any reason, setup the default
|
||||
# formatting. If the above succeeds, this wil result in a no-op.
|
||||
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
|
||||
|
||||
# Suppress overly verbose logs from libraries that aren't helpful
|
||||
logging.getLogger('requests').setLevel(logging.WARNING)
|
||||
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
||||
logging.getLogger('aiohttp.access').setLevel(logging.WARNING)
|
||||
|
||||
try:
|
||||
from colorlog import ColoredFormatter
|
||||
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
|
||||
colorfmt,
|
||||
datefmt=datefmt,
|
||||
reset=True,
|
||||
log_colors={
|
||||
'DEBUG': 'cyan',
|
||||
'INFO': 'green',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'red',
|
||||
}
|
||||
))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Log errors to a file if we have write access to file or config dir
|
||||
if log_file is None:
|
||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv
|
|||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['abodepy==0.12.3']
|
||||
REQUIREMENTS = ['abodepy==0.13.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -27,6 +27,7 @@ CONF_ATTRIBUTION = "Data provided by goabode.com"
|
|||
CONF_POLLING = 'polling'
|
||||
|
||||
DOMAIN = 'abode'
|
||||
DEFAULT_CACHEDB = './abodepy_cache.pickle'
|
||||
|
||||
NOTIFICATION_ID = 'abode_notification'
|
||||
NOTIFICATION_TITLE = 'Abode Security Setup'
|
||||
|
@ -87,12 +88,13 @@ ABODE_PLATFORMS = [
|
|||
class AbodeSystem(object):
|
||||
"""Abode System class."""
|
||||
|
||||
def __init__(self, username, password, name, polling, exclude, lights):
|
||||
def __init__(self, username, password, cache,
|
||||
name, polling, exclude, lights):
|
||||
"""Initialize the system."""
|
||||
import abodepy
|
||||
self.abode = abodepy.Abode(
|
||||
username, password, auto_login=True, get_devices=True,
|
||||
get_automations=True)
|
||||
get_automations=True, cache_path=cache)
|
||||
self.name = name
|
||||
self.polling = polling
|
||||
self.exclude = exclude
|
||||
|
@ -129,8 +131,9 @@ def setup(hass, config):
|
|||
lights = conf.get(CONF_LIGHTS)
|
||||
|
||||
try:
|
||||
cache = hass.config.path(DEFAULT_CACHEDB)
|
||||
hass.data[DOMAIN] = AbodeSystem(
|
||||
username, password, name, polling, exclude, lights)
|
||||
username, password, cache, name, polling, exclude, lights)
|
||||
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
|
||||
|
||||
|
|
|
@ -1471,6 +1471,7 @@ async def async_api_adjust_target_temp(hass, config, request, entity):
|
|||
async def async_api_set_thermostat_mode(hass, config, request, entity):
|
||||
"""Process a set thermostat mode request."""
|
||||
mode = request[API_PAYLOAD]['thermostatMode']
|
||||
mode = mode if isinstance(mode, str) else mode['value']
|
||||
|
||||
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
|
||||
# Work around a pylint false positive due to
|
||||
|
|
|
@ -46,6 +46,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
|||
self._vehicle = vehicle
|
||||
self._attribute = attribute
|
||||
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
|
||||
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
|
||||
self._sensor_name = sensor_name
|
||||
self._device_class = device_class
|
||||
self._state = None
|
||||
|
@ -55,6 +56,11 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
|||
"""Data update is triggered from BMWConnectedDriveEntity."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of the binary sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
|
|
|
@ -32,6 +32,7 @@ class HiveBinarySensorEntity(BinarySensorDevice):
|
|||
self.device_type = hivedevice["HA_DeviceType"]
|
||||
self.node_device_type = hivedevice["Hive_DeviceType"]
|
||||
self.session = hivesession
|
||||
self.attributes = {}
|
||||
self.data_updatesource = '{}.{}'.format(self.device_type,
|
||||
self.node_id)
|
||||
|
||||
|
@ -52,6 +53,11 @@ class HiveBinarySensorEntity(BinarySensorDevice):
|
|||
"""Return the name of the binary sensor."""
|
||||
return self.node_name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Show Device Attributes."""
|
||||
return self.attributes
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
|
@ -61,3 +67,5 @@ class HiveBinarySensorEntity(BinarySensorDevice):
|
|||
def update(self):
|
||||
"""Update all Node data from Hive."""
|
||||
self.session.core.update_data(self.node_id)
|
||||
self.attributes = self.session.attributes.state_attributes(
|
||||
self.node_id)
|
||||
|
|
|
@ -7,7 +7,7 @@ https://home-assistant.io/components/maxcube/
|
|||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.maxcube import MAXCUBE_HANDLE
|
||||
from homeassistant.components.maxcube import DATA_KEY
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -15,16 +15,17 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Iterate through all MAX! Devices and add window shutters."""
|
||||
cube = hass.data[MAXCUBE_HANDLE].cube
|
||||
devices = []
|
||||
for handler in hass.data[DATA_KEY].values():
|
||||
cube = handler.cube
|
||||
for device in cube.devices:
|
||||
name = "{} {}".format(
|
||||
cube.room_by_id(device.room_id).name, device.name)
|
||||
|
||||
for device in cube.devices:
|
||||
name = "{} {}".format(
|
||||
cube.room_by_id(device.room_id).name, device.name)
|
||||
|
||||
# Only add Window Shutters
|
||||
if cube.is_windowshutter(device):
|
||||
devices.append(MaxCubeShutter(hass, name, device.rf_address))
|
||||
# Only add Window Shutters
|
||||
if cube.is_windowshutter(device):
|
||||
devices.append(
|
||||
MaxCubeShutter(handler, name, device.rf_address))
|
||||
|
||||
if devices:
|
||||
add_devices(devices)
|
||||
|
@ -33,12 +34,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class MaxCubeShutter(BinarySensorDevice):
|
||||
"""Representation of a MAX! Cube Binary Sensor device."""
|
||||
|
||||
def __init__(self, hass, name, rf_address):
|
||||
def __init__(self, handler, name, rf_address):
|
||||
"""Initialize MAX! Cube BinarySensorDevice."""
|
||||
self._name = name
|
||||
self._sensor_type = 'window'
|
||||
self._rf_address = rf_address
|
||||
self._cubehandle = hass.data[MAXCUBE_HANDLE]
|
||||
self._cubehandle = handler
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
|
|
|
@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
DOMAIN = 'bmw_connected_drive'
|
||||
CONF_REGION = 'region'
|
||||
|
||||
ATTR_VIN = 'vin'
|
||||
|
||||
ACCOUNT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
|
@ -35,35 +35,40 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
},
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_VIN): cv.string,
|
||||
})
|
||||
|
||||
|
||||
BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor']
|
||||
UPDATE_INTERVAL = 5 # in minutes
|
||||
|
||||
SERVICE_UPDATE_STATE = 'update_state'
|
||||
|
||||
def setup(hass, config):
|
||||
_SERVICE_MAP = {
|
||||
'light_flash': 'trigger_remote_light_flash',
|
||||
'sound_horn': 'trigger_remote_horn',
|
||||
'activate_air_conditioning': 'trigger_remote_air_conditioning',
|
||||
}
|
||||
|
||||
|
||||
def setup(hass, config: dict):
|
||||
"""Set up the BMW connected drive components."""
|
||||
accounts = []
|
||||
for name, account_config in config[DOMAIN].items():
|
||||
username = account_config[CONF_USERNAME]
|
||||
password = account_config[CONF_PASSWORD]
|
||||
region = account_config[CONF_REGION]
|
||||
_LOGGER.debug('Adding new account %s', name)
|
||||
bimmer = BMWConnectedDriveAccount(username, password, region, name)
|
||||
accounts.append(bimmer)
|
||||
|
||||
# update every UPDATE_INTERVAL minutes, starting now
|
||||
# this should even out the load on the servers
|
||||
|
||||
now = datetime.datetime.now()
|
||||
track_utc_time_change(
|
||||
hass, bimmer.update,
|
||||
minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
|
||||
second=now.second)
|
||||
accounts.append(setup_account(account_config, hass, name))
|
||||
|
||||
hass.data[DOMAIN] = accounts
|
||||
|
||||
for account in accounts:
|
||||
account.update()
|
||||
def _update_all(call) -> None:
|
||||
"""Update all BMW accounts."""
|
||||
for cd_account in hass.data[DOMAIN]:
|
||||
cd_account.update()
|
||||
|
||||
# Service to manually trigger updates for all accounts.
|
||||
hass.services.register(DOMAIN, SERVICE_UPDATE_STATE, _update_all)
|
||||
|
||||
_update_all(None)
|
||||
|
||||
for component in BMW_COMPONENTS:
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
@ -71,6 +76,48 @@ def setup(hass, config):
|
|||
return True
|
||||
|
||||
|
||||
def setup_account(account_config: dict, hass, name: str) \
|
||||
-> 'BMWConnectedDriveAccount':
|
||||
"""Set up a new BMWConnectedDriveAccount based on the config."""
|
||||
username = account_config[CONF_USERNAME]
|
||||
password = account_config[CONF_PASSWORD]
|
||||
region = account_config[CONF_REGION]
|
||||
_LOGGER.debug('Adding new account %s', name)
|
||||
cd_account = BMWConnectedDriveAccount(username, password, region, name)
|
||||
|
||||
def execute_service(call):
|
||||
"""Execute a service for a vehicle.
|
||||
|
||||
This must be a member function as we need access to the cd_account
|
||||
object here.
|
||||
"""
|
||||
vin = call.data[ATTR_VIN]
|
||||
vehicle = cd_account.account.get_vehicle(vin)
|
||||
if not vehicle:
|
||||
_LOGGER.error('Could not find a vehicle for VIN "%s"!', vin)
|
||||
return
|
||||
function_name = _SERVICE_MAP[call.service]
|
||||
function_call = getattr(vehicle.remote_services, function_name)
|
||||
function_call()
|
||||
|
||||
# register the remote services
|
||||
for service in _SERVICE_MAP:
|
||||
hass.services.register(
|
||||
DOMAIN, service,
|
||||
execute_service,
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
# update every UPDATE_INTERVAL minutes, starting now
|
||||
# this should even out the load on the servers
|
||||
now = datetime.datetime.now()
|
||||
track_utc_time_change(
|
||||
hass, cd_account.update,
|
||||
minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
|
||||
second=now.second)
|
||||
|
||||
return cd_account
|
||||
|
||||
|
||||
class BMWConnectedDriveAccount(object):
|
||||
"""Representation of a BMW vehicle."""
|
||||
|
42
homeassistant/components/bmw_connected_drive/services.yaml
Normal file
42
homeassistant/components/bmw_connected_drive/services.yaml
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Describes the format for available services for bmw_connected_drive
|
||||
#
|
||||
# The services related to locking/unlocking are implemented in the lock
|
||||
# component to avoid redundancy.
|
||||
|
||||
light_flash:
|
||||
description: >
|
||||
Flash the lights of the vehicle. The vehicle is identified via the vin
|
||||
(see below).
|
||||
fields:
|
||||
vin:
|
||||
description: >
|
||||
The vehicle identification number (VIN) of the vehicle, 17 characters
|
||||
example: WBANXXXXXX1234567
|
||||
|
||||
sound_horn:
|
||||
description: >
|
||||
Sound the horn of the vehicle. The vehicle is identified via the vin
|
||||
(see below).
|
||||
fields:
|
||||
vin:
|
||||
description: >
|
||||
The vehicle identification number (VIN) of the vehicle, 17 characters
|
||||
example: WBANXXXXXX1234567
|
||||
|
||||
activate_air_conditioning:
|
||||
description: >
|
||||
Start the air conditioning of the vehicle. What exactly is started here
|
||||
depends on the type of vehicle. It might range from just ventilation over
|
||||
auxilary heating to real air conditioning. The vehicle is identified via
|
||||
the vin (see below).
|
||||
fields:
|
||||
vin:
|
||||
description: >
|
||||
The vehicle identification number (VIN) of the vehicle, 17 characters
|
||||
example: WBANXXXXXX1234567
|
||||
|
||||
update_state:
|
||||
description: >
|
||||
Fetch the last state of the vehicles of all your accounts from the BMW
|
||||
server. This does *not* trigger an update from the vehicle, it just gets
|
||||
the data from the BMW servers. This service does not require any attributes.
|
|
@ -11,6 +11,7 @@ from datetime import timedelta
|
|||
from homeassistant.components.calendar import CalendarEventDevice
|
||||
from homeassistant.components.google import (
|
||||
CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE,
|
||||
CONF_IGNORE_AVAILABILITY, CONF_SEARCH,
|
||||
GoogleCalendarService)
|
||||
from homeassistant.util import Throttle, dt
|
||||
|
||||
|
@ -18,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
DEFAULT_GOOGLE_SEARCH_PARAMS = {
|
||||
'orderBy': 'startTime',
|
||||
'maxResults': 1,
|
||||
'maxResults': 5,
|
||||
'singleEvents': True,
|
||||
}
|
||||
|
||||
|
@ -45,18 +46,22 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
|
|||
def __init__(self, hass, calendar_service, calendar, data):
|
||||
"""Create the Calendar event device."""
|
||||
self.data = GoogleCalendarData(calendar_service, calendar,
|
||||
data.get('search', None))
|
||||
data.get(CONF_SEARCH),
|
||||
data.get(CONF_IGNORE_AVAILABILITY))
|
||||
|
||||
super().__init__(hass, data)
|
||||
|
||||
|
||||
class GoogleCalendarData(object):
|
||||
"""Class to utilize calendar service object to get next event."""
|
||||
|
||||
def __init__(self, calendar_service, calendar_id, search=None):
|
||||
def __init__(self, calendar_service, calendar_id, search,
|
||||
ignore_availability):
|
||||
"""Set up how we are going to search the google calendar."""
|
||||
self.calendar_service = calendar_service
|
||||
self.calendar_id = calendar_id
|
||||
self.search = search
|
||||
self.ignore_availability = ignore_availability
|
||||
self.event = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
|
@ -80,5 +85,17 @@ class GoogleCalendarData(object):
|
|||
result = events.list(**params).execute()
|
||||
|
||||
items = result.get('items', [])
|
||||
self.event = items[0] if len(items) == 1 else None
|
||||
|
||||
new_event = None
|
||||
for item in items:
|
||||
if (not self.ignore_availability
|
||||
and 'transparency' in item.keys()):
|
||||
if item['transparency'] == 'opaque':
|
||||
new_event = item
|
||||
break
|
||||
else:
|
||||
new_event = item
|
||||
break
|
||||
|
||||
self.event = new_event
|
||||
return True
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
# Describes the format for available calendar services
|
||||
|
||||
todoist:
|
||||
new_task:
|
||||
description: Create a new task and add it to a project.
|
||||
fields:
|
||||
content:
|
||||
description: The name of the task (Required).
|
||||
example: Pick up the mail
|
||||
project:
|
||||
description: The name of the project this task should belong to. Defaults to Inbox (Optional).
|
||||
example: Errands
|
||||
labels:
|
||||
description: Any labels that you want to apply to this task, separated by a comma (Optional).
|
||||
example: Chores,Deliveries
|
||||
priority:
|
||||
description: The priority of this task, from 1 (normal) to 4 (urgent) (Optional).
|
||||
example: 2
|
||||
due_date:
|
||||
description: The day this task is due, in format YYYY-MM-DD (Optional).
|
||||
example: "2018-04-01"
|
||||
todoist_new_task:
|
||||
description: Create a new task and add it to a project.
|
||||
fields:
|
||||
content:
|
||||
description: The name of the task.
|
||||
example: Pick up the mail
|
||||
project:
|
||||
description: The name of the project this task should belong to. Defaults to Inbox.
|
||||
example: Errands
|
||||
labels:
|
||||
description: Any labels that you want to apply to this task, separated by a comma.
|
||||
example: Chores,Deliveries
|
||||
priority:
|
||||
description: The priority of this task, from 1 (normal) to 4 (urgent).
|
||||
example: 2
|
||||
due_date_string:
|
||||
description: The day this task is due, in natural language.
|
||||
example: "tomorrow"
|
||||
due_date_lang:
|
||||
description: The language of due_date_string.
|
||||
example: "en"
|
||||
due_date:
|
||||
description: The day this task is due, in format YYYY-MM-DD.
|
||||
example: "2018-04-01"
|
||||
|
|
|
@ -41,6 +41,14 @@ CONTENT = 'content'
|
|||
DESCRIPTION = 'description'
|
||||
# Calendar Platform: Used in the '_get_date()' method
|
||||
DATETIME = 'dateTime'
|
||||
# Service Call: When is this task due (in natural language)?
|
||||
DUE_DATE_STRING = 'due_date_string'
|
||||
# Service Call: The language of DUE_DATE_STRING
|
||||
DUE_DATE_LANG = 'due_date_lang'
|
||||
# Service Call: The available options of DUE_DATE_LANG
|
||||
DUE_DATE_VALID_LANGS = ['en', 'da', 'pl', 'zh', 'ko', 'de',
|
||||
'pt', 'ja', 'it', 'fr', 'sv', 'ru',
|
||||
'es', 'nl']
|
||||
# Attribute: When is this task due?
|
||||
# Service Call: When is this task due?
|
||||
DUE_DATE = 'due_date'
|
||||
|
@ -83,7 +91,11 @@ NEW_TASK_SERVICE_SCHEMA = vol.Schema({
|
|||
vol.Optional(PROJECT_NAME, default='inbox'): vol.All(cv.string, vol.Lower),
|
||||
vol.Optional(LABELS): cv.ensure_list_csv,
|
||||
vol.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)),
|
||||
vol.Optional(DUE_DATE): cv.string,
|
||||
|
||||
vol.Exclusive(DUE_DATE_STRING, 'due_date'): cv.string,
|
||||
vol.Optional(DUE_DATE_LANG):
|
||||
vol.All(cv.string, vol.In(DUE_DATE_VALID_LANGS)),
|
||||
vol.Exclusive(DUE_DATE, 'due_date'): cv.string,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
@ -186,6 +198,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
if PRIORITY in call.data:
|
||||
item.update(priority=call.data[PRIORITY])
|
||||
|
||||
if DUE_DATE_STRING in call.data:
|
||||
item.update(date_string=call.data[DUE_DATE_STRING])
|
||||
|
||||
if DUE_DATE_LANG in call.data:
|
||||
item.update(date_lang=call.data[DUE_DATE_LANG])
|
||||
|
||||
if DUE_DATE in call.data:
|
||||
due_date = dt.parse_datetime(call.data[DUE_DATE])
|
||||
if due_date is None:
|
||||
|
|
|
@ -40,6 +40,7 @@ STATE_HEAT = 'heat'
|
|||
STATE_COOL = 'cool'
|
||||
STATE_IDLE = 'idle'
|
||||
STATE_AUTO = 'auto'
|
||||
STATE_MANUAL = 'manual'
|
||||
STATE_DRY = 'dry'
|
||||
STATE_FAN_ONLY = 'fan_only'
|
||||
STATE_ECO = 'eco'
|
||||
|
|
153
homeassistant/components/climate/fritzbox.py
Executable file
153
homeassistant/components/climate/fritzbox.py
Executable file
|
@ -0,0 +1,153 @@
|
|||
"""
|
||||
Support for AVM Fritz!Box smarthome thermostate devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
http://home-assistant.io/components/climate.fritzbox/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN
|
||||
from homeassistant.components.fritzbox import (
|
||||
ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_LOCKED)
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS)
|
||||
|
||||
DEPENDENCIES = ['fritzbox']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE)
|
||||
|
||||
OPERATION_LIST = [STATE_HEAT, STATE_ECO]
|
||||
|
||||
MIN_TEMPERATURE = 8
|
||||
MAX_TEMPERATURE = 28
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Fritzbox smarthome thermostat platform."""
|
||||
devices = []
|
||||
fritz_list = hass.data[FRITZBOX_DOMAIN]
|
||||
|
||||
for fritz in fritz_list:
|
||||
device_list = fritz.get_devices()
|
||||
for device in device_list:
|
||||
if device.has_thermostat:
|
||||
devices.append(FritzboxThermostat(device, fritz))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class FritzboxThermostat(ClimateDevice):
|
||||
"""The thermostat class for Fritzbox smarthome thermostates."""
|
||||
|
||||
def __init__(self, device, fritz):
|
||||
"""Initialize the thermostat."""
|
||||
self._device = device
|
||||
self._fritz = fritz
|
||||
self._current_temperature = self._device.actual_temperature
|
||||
self._target_temperature = self._device.target_temperature
|
||||
self._comfort_temperature = self._device.comfort_temperature
|
||||
self._eco_temperature = self._device.eco_temperature
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if thermostat is available."""
|
||||
return self._device.present
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement that is used."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return precision 0.5."""
|
||||
return PRECISION_HALVES
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if ATTR_OPERATION_MODE in kwargs:
|
||||
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
|
||||
self.set_operation_mode(operation_mode)
|
||||
elif ATTR_TEMPERATURE in kwargs:
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
self._device.set_target_temperature(temperature)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return the current operation mode."""
|
||||
if self._target_temperature == self._comfort_temperature:
|
||||
return STATE_HEAT
|
||||
elif self._target_temperature == self._eco_temperature:
|
||||
return STATE_ECO
|
||||
return STATE_MANUAL
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return OPERATION_LIST
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new operation mode."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
self.set_temperature(temperature=self._comfort_temperature)
|
||||
elif operation_mode == STATE_ECO:
|
||||
self.set_temperature(temperature=self._eco_temperature)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return MIN_TEMPERATURE
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return MAX_TEMPERATURE
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
attrs = {
|
||||
ATTR_STATE_DEVICE_LOCKED: self._device.device_lock,
|
||||
ATTR_STATE_LOCKED: self._device.lock,
|
||||
ATTR_STATE_BATTERY_LOW: self._device.battery_low,
|
||||
}
|
||||
return attrs
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
try:
|
||||
self._device.update()
|
||||
self._current_temperature = self._device.actual_temperature
|
||||
self._target_temperature = self._device.target_temperature
|
||||
self._comfort_temperature = self._device.comfort_temperature
|
||||
self._eco_temperature = self._device.eco_temperature
|
||||
except requests.exceptions.HTTPError as ex:
|
||||
_LOGGER.warning("Fritzbox connection error: %s", ex)
|
||||
self._fritz.login()
|
|
@ -38,7 +38,10 @@ class HiveClimateEntity(ClimateDevice):
|
|||
self.node_id = hivedevice["Hive_NodeID"]
|
||||
self.node_name = hivedevice["Hive_NodeName"]
|
||||
self.device_type = hivedevice["HA_DeviceType"]
|
||||
if self.device_type == "Heating":
|
||||
self.thermostat_node_id = hivedevice["Thermostat_NodeID"]
|
||||
self.session = hivesession
|
||||
self.attributes = {}
|
||||
self.data_updatesource = '{}.{}'.format(self.device_type,
|
||||
self.node_id)
|
||||
|
||||
|
@ -71,6 +74,11 @@ class HiveClimateEntity(ClimateDevice):
|
|||
friendly_name = "Hot Water"
|
||||
return friendly_name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Show Device Attributes."""
|
||||
return self.attributes
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
|
@ -175,4 +183,9 @@ class HiveClimateEntity(ClimateDevice):
|
|||
|
||||
def update(self):
|
||||
"""Update all Node data from Hive."""
|
||||
node = self.node_id
|
||||
if self.device_type == "Heating":
|
||||
node = self.thermostat_node_id
|
||||
|
||||
self.session.core.update_data(self.node_id)
|
||||
self.attributes = self.session.attributes.state_attributes(node)
|
||||
|
|
|
@ -10,7 +10,7 @@ import logging
|
|||
from homeassistant.components.climate import (
|
||||
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.components.maxcube import MAXCUBE_HANDLE
|
||||
from homeassistant.components.maxcube import DATA_KEY
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -24,16 +24,16 @@ SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
|||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Iterate through all MAX! Devices and add thermostats."""
|
||||
cube = hass.data[MAXCUBE_HANDLE].cube
|
||||
|
||||
devices = []
|
||||
for handler in hass.data[DATA_KEY].values():
|
||||
cube = handler.cube
|
||||
for device in cube.devices:
|
||||
name = '{} {}'.format(
|
||||
cube.room_by_id(device.room_id).name, device.name)
|
||||
|
||||
for device in cube.devices:
|
||||
name = '{} {}'.format(
|
||||
cube.room_by_id(device.room_id).name, device.name)
|
||||
|
||||
if cube.is_thermostat(device) or cube.is_wallthermostat(device):
|
||||
devices.append(MaxCubeClimate(hass, name, device.rf_address))
|
||||
if cube.is_thermostat(device) or cube.is_wallthermostat(device):
|
||||
devices.append(
|
||||
MaxCubeClimate(handler, name, device.rf_address))
|
||||
|
||||
if devices:
|
||||
add_devices(devices)
|
||||
|
@ -42,14 +42,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class MaxCubeClimate(ClimateDevice):
|
||||
"""MAX! Cube ClimateDevice."""
|
||||
|
||||
def __init__(self, hass, name, rf_address):
|
||||
def __init__(self, handler, name, rf_address):
|
||||
"""Initialize MAX! Cube ClimateDevice."""
|
||||
self._name = name
|
||||
self._unit_of_measurement = TEMP_CELSIUS
|
||||
self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST,
|
||||
STATE_VACATION]
|
||||
self._rf_address = rf_address
|
||||
self._cubehandle = hass.data[MAXCUBE_HANDLE]
|
||||
self._cubehandle = handler
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
|
148
homeassistant/components/climate/modbus.py
Normal file
148
homeassistant/components/climate/modbus.py
Normal file
|
@ -0,0 +1,148 @@
|
|||
"""
|
||||
Platform for a Generic Modbus Thermostat.
|
||||
|
||||
This uses a setpoint and process
|
||||
value within the controller, so both the current temperature register and the
|
||||
target temperature register need to be configured.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.modbus/
|
||||
"""
|
||||
import logging
|
||||
import struct
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_SLAVE, ATTR_TEMPERATURE)
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
|
||||
|
||||
import homeassistant.components.modbus as modbus
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['modbus']
|
||||
|
||||
# Parameters not defined by homeassistant.const
|
||||
CONF_TARGET_TEMP = 'target_temp_register'
|
||||
CONF_CURRENT_TEMP = 'current_temp_register'
|
||||
CONF_DATA_TYPE = 'data_type'
|
||||
CONF_COUNT = 'data_count'
|
||||
CONF_PRECISION = 'precision'
|
||||
|
||||
DATA_TYPE_INT = 'int'
|
||||
DATA_TYPE_UINT = 'uint'
|
||||
DATA_TYPE_FLOAT = 'float'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_SLAVE): cv.positive_int,
|
||||
vol.Required(CONF_TARGET_TEMP): cv.positive_int,
|
||||
vol.Required(CONF_CURRENT_TEMP): cv.positive_int,
|
||||
vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT):
|
||||
vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]),
|
||||
vol.Optional(CONF_COUNT, default=2): cv.positive_int,
|
||||
vol.Optional(CONF_PRECISION, default=1): cv.positive_int
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Modbus Thermostat Platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
modbus_slave = config.get(CONF_SLAVE)
|
||||
target_temp_register = config.get(CONF_TARGET_TEMP)
|
||||
current_temp_register = config.get(CONF_CURRENT_TEMP)
|
||||
data_type = config.get(CONF_DATA_TYPE)
|
||||
count = config.get(CONF_COUNT)
|
||||
precision = config.get(CONF_PRECISION)
|
||||
|
||||
add_devices([ModbusThermostat(name, modbus_slave,
|
||||
target_temp_register, current_temp_register,
|
||||
data_type, count, precision)], True)
|
||||
|
||||
|
||||
class ModbusThermostat(ClimateDevice):
|
||||
"""Representation of a Modbus Thermostat."""
|
||||
|
||||
def __init__(self, name, modbus_slave, target_temp_register,
|
||||
current_temp_register, data_type, count, precision):
|
||||
"""Initialize the unit."""
|
||||
self._name = name
|
||||
self._slave = modbus_slave
|
||||
self._target_temperature_register = target_temp_register
|
||||
self._current_temperature_register = current_temp_register
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
self._data_type = data_type
|
||||
self._count = int(count)
|
||||
self._precision = precision
|
||||
self._structure = '>f'
|
||||
|
||||
data_types = {DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'},
|
||||
DATA_TYPE_UINT: {1: 'H', 2: 'I', 4: 'Q'},
|
||||
DATA_TYPE_FLOAT: {1: 'e', 2: 'f', 4: 'd'}}
|
||||
|
||||
self._structure = '>{}'.format(data_types[self._data_type]
|
||||
[self._count])
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
def update(self):
|
||||
"""Update Target & Current Temperature."""
|
||||
self._target_temperature = self.read_register(
|
||||
self._target_temperature_register)
|
||||
self._current_temperature = self.read_register(
|
||||
self._current_temperature_register)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the climate device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if target_temperature is None:
|
||||
return
|
||||
byte_string = struct.pack(self._structure, target_temperature)
|
||||
register_value = struct.unpack('>h', byte_string[0:2])[0]
|
||||
|
||||
try:
|
||||
self.write_register(self._target_temperature_register,
|
||||
register_value)
|
||||
except AttributeError as ex:
|
||||
_LOGGER.error(ex)
|
||||
|
||||
def read_register(self, register):
|
||||
"""Read holding register using the modbus hub slave."""
|
||||
try:
|
||||
result = modbus.HUB.read_holding_registers(self._slave, register,
|
||||
self._count)
|
||||
except AttributeError as ex:
|
||||
_LOGGER.error(ex)
|
||||
byte_string = b''.join(
|
||||
[x.to_bytes(2, byteorder='big') for x in result.registers])
|
||||
val = struct.unpack(self._structure, byte_string)[0]
|
||||
register_value = format(val, '.{}f'.format(self._precision))
|
||||
return register_value
|
||||
|
||||
def write_register(self, register, value):
|
||||
"""Write register using the modbus hub slave."""
|
||||
modbus.HUB.write_registers(self._slave, register, [value, 0])
|
|
@ -187,6 +187,11 @@ class NestThermostat(ClimateDevice):
|
|||
device_mode = operation_mode
|
||||
elif operation_mode == STATE_AUTO:
|
||||
device_mode = NEST_MODE_HEAT_COOL
|
||||
else:
|
||||
device_mode = STATE_OFF
|
||||
_LOGGER.error(
|
||||
"An error occurred while setting device mode. "
|
||||
"Invalid operation mode: %s", operation_mode)
|
||||
self.device.mode = device_mode
|
||||
|
||||
@property
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
"""Http views to control the config manager."""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
|
||||
|
||||
REQUIREMENTS = ['voluptuous-serialize==1']
|
||||
|
@ -16,15 +15,17 @@ def async_setup(hass):
|
|||
"""Enable the Home Assistant views."""
|
||||
hass.http.register_view(ConfigManagerEntryIndexView)
|
||||
hass.http.register_view(ConfigManagerEntryResourceView)
|
||||
hass.http.register_view(ConfigManagerFlowIndexView)
|
||||
hass.http.register_view(ConfigManagerFlowResourceView)
|
||||
hass.http.register_view(
|
||||
ConfigManagerFlowIndexView(hass.config_entries.flow))
|
||||
hass.http.register_view(
|
||||
ConfigManagerFlowResourceView(hass.config_entries.flow))
|
||||
hass.http.register_view(ConfigManagerAvailableFlowView)
|
||||
return True
|
||||
|
||||
|
||||
def _prepare_json(result):
|
||||
"""Convert result for JSON."""
|
||||
if result['type'] != config_entries.RESULT_TYPE_FORM:
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
|
||||
return result
|
||||
|
||||
import voluptuous_serialize
|
||||
|
@ -78,7 +79,7 @@ class ConfigManagerEntryResourceView(HomeAssistantView):
|
|||
return self.json(result)
|
||||
|
||||
|
||||
class ConfigManagerFlowIndexView(HomeAssistantView):
|
||||
class ConfigManagerFlowIndexView(FlowManagerIndexView):
|
||||
"""View to create config flows."""
|
||||
|
||||
url = '/api/config/config_entries/flow'
|
||||
|
@ -94,81 +95,16 @@ class ConfigManagerFlowIndexView(HomeAssistantView):
|
|||
hass = request.app['hass']
|
||||
|
||||
return self.json([
|
||||
flow for flow in hass.config_entries.flow.async_progress()
|
||||
if flow['source'] != config_entries.SOURCE_USER])
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('domain'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
"""Handle a POST request."""
|
||||
hass = request.app['hass']
|
||||
|
||||
try:
|
||||
result = yield from hass.config_entries.flow.async_init(
|
||||
data['domain'])
|
||||
except config_entries.UnknownHandler:
|
||||
return self.json_message('Invalid handler specified', 404)
|
||||
except config_entries.UnknownStep:
|
||||
return self.json_message('Handler does not support init', 400)
|
||||
|
||||
result = _prepare_json(result)
|
||||
|
||||
return self.json(result)
|
||||
flw for flw in hass.config_entries.flow.async_progress()
|
||||
if flw['source'] != data_entry_flow.SOURCE_USER])
|
||||
|
||||
|
||||
class ConfigManagerFlowResourceView(HomeAssistantView):
|
||||
class ConfigManagerFlowResourceView(FlowManagerResourceView):
|
||||
"""View to interact with the flow manager."""
|
||||
|
||||
url = '/api/config/config_entries/flow/{flow_id}'
|
||||
name = 'api:config:config_entries:flow:resource'
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, flow_id):
|
||||
"""Get the current state of a flow."""
|
||||
hass = request.app['hass']
|
||||
|
||||
try:
|
||||
result = yield from hass.config_entries.flow.async_configure(
|
||||
flow_id)
|
||||
except config_entries.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
|
||||
result = _prepare_json(result)
|
||||
|
||||
return self.json(result)
|
||||
|
||||
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
|
||||
@asyncio.coroutine
|
||||
def post(self, request, flow_id, data):
|
||||
"""Handle a POST request."""
|
||||
hass = request.app['hass']
|
||||
|
||||
try:
|
||||
result = yield from hass.config_entries.flow.async_configure(
|
||||
flow_id, data)
|
||||
except config_entries.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
except vol.Invalid:
|
||||
return self.json_message('User input malformed', 400)
|
||||
|
||||
result = _prepare_json(result)
|
||||
|
||||
return self.json(result)
|
||||
|
||||
@asyncio.coroutine
|
||||
def delete(self, request, flow_id):
|
||||
"""Cancel a flow in progress."""
|
||||
hass = request.app['hass']
|
||||
|
||||
try:
|
||||
hass.config_entries.flow.async_abort(flow_id)
|
||||
except config_entries.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
|
||||
return self.json_message('Flow aborted')
|
||||
|
||||
|
||||
class ConfigManagerAvailableFlowView(HomeAssistantView):
|
||||
"""View to query available flows."""
|
||||
|
|
|
@ -18,30 +18,31 @@ import homeassistant.helpers.config_validation as cv
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DISTANCE_SENSOR = "distance_sensor"
|
||||
ATTR_DOOR_STATE = "door_state"
|
||||
ATTR_SIGNAL_STRENGTH = "wifi_signal"
|
||||
ATTR_DISTANCE_SENSOR = 'distance_sensor'
|
||||
ATTR_DOOR_STATE = 'door_state'
|
||||
ATTR_SIGNAL_STRENGTH = 'wifi_signal'
|
||||
|
||||
CONF_DEVICEKEY = "device_key"
|
||||
CONF_DEVICE_ID = 'device_id'
|
||||
CONF_DEVICE_KEY = 'device_key'
|
||||
|
||||
DEFAULT_NAME = 'OpenGarage'
|
||||
DEFAULT_PORT = 80
|
||||
|
||||
STATE_CLOSING = "closing"
|
||||
STATE_OFFLINE = "offline"
|
||||
STATE_OPENING = "opening"
|
||||
STATE_STOPPED = "stopped"
|
||||
STATE_CLOSING = 'closing'
|
||||
STATE_OFFLINE = 'offline'
|
||||
STATE_OPENING = 'opening'
|
||||
STATE_STOPPED = 'stopped'
|
||||
|
||||
STATES_MAP = {
|
||||
0: STATE_CLOSED,
|
||||
1: STATE_OPEN
|
||||
1: STATE_OPEN,
|
||||
}
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICEKEY): cv.string,
|
||||
vol.Required(CONF_DEVICE_KEY): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_NAME): cv.string
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
@ -50,7 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up OpenGarage covers."""
|
||||
"""Set up the OpenGarage covers."""
|
||||
covers = []
|
||||
devices = config.get(CONF_COVERS)
|
||||
|
||||
|
@ -59,8 +60,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
CONF_NAME: device_config.get(CONF_NAME),
|
||||
CONF_HOST: device_config.get(CONF_HOST),
|
||||
CONF_PORT: device_config.get(CONF_PORT),
|
||||
"device_id": device_config.get(CONF_DEVICE, device_id),
|
||||
CONF_DEVICEKEY: device_config.get(CONF_DEVICEKEY)
|
||||
CONF_DEVICE_ID: device_config.get(CONF_DEVICE, device_id),
|
||||
CONF_DEVICE_KEY: device_config.get(CONF_DEVICE_KEY)
|
||||
}
|
||||
|
||||
covers.append(OpenGarageCover(hass, args))
|
||||
|
@ -79,8 +80,8 @@ class OpenGarageCover(CoverDevice):
|
|||
self.hass = hass
|
||||
self._name = args[CONF_NAME]
|
||||
self.device_id = args['device_id']
|
||||
self._devicekey = args[CONF_DEVICEKEY]
|
||||
self._state = STATE_UNKNOWN
|
||||
self._device_key = args[CONF_DEVICE_KEY]
|
||||
self._state = None
|
||||
self._state_before_move = None
|
||||
self.dist = None
|
||||
self.signal = None
|
||||
|
@ -138,8 +139,8 @@ class OpenGarageCover(CoverDevice):
|
|||
try:
|
||||
status = self._get_status()
|
||||
if self._name is None:
|
||||
if status["name"] is not None:
|
||||
self._name = status["name"]
|
||||
if status['name'] is not None:
|
||||
self._name = status['name']
|
||||
state = STATES_MAP.get(status.get('door'), STATE_UNKNOWN)
|
||||
if self._state_before_move is not None:
|
||||
if self._state_before_move != state:
|
||||
|
@ -152,7 +153,7 @@ class OpenGarageCover(CoverDevice):
|
|||
self.signal = status.get('rssi')
|
||||
self.dist = status.get('dist')
|
||||
self._available = True
|
||||
except (requests.exceptions.RequestException) as ex:
|
||||
except requests.exceptions.RequestException as ex:
|
||||
_LOGGER.error("Unable to connect to OpenGarage device: %(reason)s",
|
||||
dict(reason=ex))
|
||||
self._state = STATE_OFFLINE
|
||||
|
@ -166,15 +167,15 @@ class OpenGarageCover(CoverDevice):
|
|||
def _push_button(self):
|
||||
"""Send commands to API."""
|
||||
url = '{}/cc?dkey={}&click=1'.format(
|
||||
self.opengarage_url, self._devicekey)
|
||||
self.opengarage_url, self._device_key)
|
||||
try:
|
||||
response = requests.get(url, timeout=10).json()
|
||||
if response["result"] == 2:
|
||||
_LOGGER.error("Unable to control %s: device_key is incorrect.",
|
||||
if response['result'] == 2:
|
||||
_LOGGER.error("Unable to control %s: Device key is incorrect",
|
||||
self._name)
|
||||
self._state = self._state_before_move
|
||||
self._state_before_move = None
|
||||
except (requests.exceptions.RequestException) as ex:
|
||||
except requests.exceptions.RequestException as ex:
|
||||
_LOGGER.error("Unable to connect to OpenGarage device: %(reason)s",
|
||||
dict(reason=ex))
|
||||
self._state = self._state_before_move
|
||||
|
|
|
@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Tahoma covers."""
|
||||
"""Set up the Tahoma covers."""
|
||||
controller = hass.data[TAHOMA_DOMAIN]['controller']
|
||||
devices = []
|
||||
for device in hass.data[TAHOMA_DOMAIN]['devices']['cover']:
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"no_key": "Couldn't get an API key"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Bridge is already configured",
|
||||
"no_bridges": "No deCONZ bridges discovered",
|
||||
"one_instance_only": "Component only supports one deCONZ instance"
|
||||
}
|
||||
|
|
|
@ -4,28 +4,20 @@ Support for deCONZ devices.
|
|||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/deconz/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.discovery import SERVICE_DECONZ
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery, aiohttp_client
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client, discovery, config_validation as cv)
|
||||
from homeassistant.util.json import load_json
|
||||
|
||||
REQUIREMENTS = ['pydeconz==35']
|
||||
# Loading the config flow file will register the flow
|
||||
from .config_flow import configured_hosts
|
||||
from .const import CONFIG_FILE, DATA_DECONZ_ID, DOMAIN, _LOGGER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'deconz'
|
||||
DATA_DECONZ_ID = 'deconz_entities'
|
||||
|
||||
CONFIG_FILE = 'deconz.conf'
|
||||
REQUIREMENTS = ['pydeconz==36']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
|
@ -46,46 +38,38 @@ SERVICE_SCHEMA = vol.Schema({
|
|||
})
|
||||
|
||||
|
||||
CONFIG_INSTRUCTIONS = """
|
||||
Unlock your deCONZ gateway to register with Home Assistant.
|
||||
|
||||
1. [Go to deCONZ system settings](http://{}:{}/edit_system.html)
|
||||
2. Press "Unlock Gateway" button
|
||||
|
||||
[deCONZ platform documentation](https://home-assistant.io/components/deconz/)
|
||||
"""
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up services and configuration for deCONZ component."""
|
||||
result = False
|
||||
config_file = await hass.async_add_job(
|
||||
load_json, hass.config.path(CONFIG_FILE))
|
||||
|
||||
async def async_deconz_discovered(service, discovery_info):
|
||||
"""Call when deCONZ gateway has been found."""
|
||||
deconz_config = {}
|
||||
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
|
||||
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
|
||||
await async_request_configuration(hass, config, deconz_config)
|
||||
|
||||
if config_file:
|
||||
result = await async_setup_deconz(hass, config, config_file)
|
||||
|
||||
if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]:
|
||||
deconz_config = config[DOMAIN]
|
||||
if CONF_API_KEY in deconz_config:
|
||||
result = await async_setup_deconz(hass, config, deconz_config)
|
||||
else:
|
||||
await async_request_configuration(hass, config, deconz_config)
|
||||
return True
|
||||
|
||||
if not result:
|
||||
discovery.async_listen(hass, SERVICE_DECONZ, async_deconz_discovered)
|
||||
"""Load configuration for deCONZ component.
|
||||
|
||||
Discovery has loaded the component if DOMAIN is not present in 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]:
|
||||
deconz_config = config[DOMAIN]
|
||||
if deconz_config and not configured_hosts(hass):
|
||||
hass.async_add_job(hass.config_entries.flow.async_init(
|
||||
DOMAIN, source='import', data=deconz_config
|
||||
))
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up a deCONZ bridge for a config entry."""
|
||||
if DOMAIN in hass.data:
|
||||
_LOGGER.error(
|
||||
"Config entry failed since one deCONZ instance already exists")
|
||||
return False
|
||||
result = await async_setup_deconz(hass, None, entry.data)
|
||||
if result:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def async_setup_deconz(hass, config, deconz_config):
|
||||
"""Set up a deCONZ session.
|
||||
|
||||
|
@ -94,8 +78,8 @@ async def async_setup_deconz(hass, config, deconz_config):
|
|||
"""
|
||||
_LOGGER.debug("deCONZ config %s", deconz_config)
|
||||
from pydeconz import DeconzSession
|
||||
websession = async_get_clientsession(hass)
|
||||
deconz = DeconzSession(hass.loop, websession, **deconz_config)
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
deconz = DeconzSession(hass.loop, session, **deconz_config)
|
||||
result = await deconz.async_load_parameters()
|
||||
if result is False:
|
||||
_LOGGER.error("Failed to communicate with deCONZ")
|
||||
|
@ -152,121 +136,3 @@ async def async_setup_deconz(hass, config, deconz_config):
|
|||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown)
|
||||
return True
|
||||
|
||||
|
||||
async def async_request_configuration(hass, config, deconz_config):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
|
||||
async def async_configuration_callback(data):
|
||||
"""Set up actions to do when our configuration callback is called."""
|
||||
from pydeconz.utils import async_get_api_key
|
||||
websession = async_get_clientsession(hass)
|
||||
api_key = await async_get_api_key(websession, **deconz_config)
|
||||
if api_key:
|
||||
deconz_config[CONF_API_KEY] = api_key
|
||||
result = await async_setup_deconz(hass, config, deconz_config)
|
||||
if result:
|
||||
await hass.async_add_job(
|
||||
save_json, hass.config.path(CONFIG_FILE), deconz_config)
|
||||
configurator.async_request_done(request_id)
|
||||
return
|
||||
else:
|
||||
configurator.async_notify_errors(
|
||||
request_id, "Couldn't load configuration.")
|
||||
else:
|
||||
configurator.async_notify_errors(
|
||||
request_id, "Couldn't get an API key.")
|
||||
return
|
||||
|
||||
instructions = CONFIG_INSTRUCTIONS.format(
|
||||
deconz_config[CONF_HOST], deconz_config[CONF_PORT])
|
||||
|
||||
request_id = configurator.async_request_config(
|
||||
"deCONZ", async_configuration_callback,
|
||||
description=instructions,
|
||||
entity_picture="/static/images/logo_deconz.jpeg",
|
||||
submit_caption="I have unlocked the gateway",
|
||||
)
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class DeconzFlowHandler(config_entries.ConfigFlowHandler):
|
||||
"""Handle a deCONZ config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the deCONZ flow."""
|
||||
self.bridges = []
|
||||
self.deconz_config = {}
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle a flow start."""
|
||||
from pydeconz.utils import async_discovery
|
||||
|
||||
if DOMAIN in self.hass.data:
|
||||
return self.async_abort(
|
||||
reason='one_instance_only'
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
for bridge in self.bridges:
|
||||
if bridge[CONF_HOST] == user_input[CONF_HOST]:
|
||||
self.deconz_config = bridge
|
||||
return await self.async_step_link()
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
self.bridges = await async_discovery(session)
|
||||
|
||||
if len(self.bridges) == 1:
|
||||
self.deconz_config = self.bridges[0]
|
||||
return await self.async_step_link()
|
||||
elif len(self.bridges) > 1:
|
||||
hosts = []
|
||||
for bridge in self.bridges:
|
||||
hosts.append(bridge[CONF_HOST])
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_HOST): vol.In(hosts)
|
||||
})
|
||||
)
|
||||
|
||||
return self.async_abort(
|
||||
reason='no_bridges'
|
||||
)
|
||||
|
||||
async def async_step_link(self, user_input=None):
|
||||
"""Attempt to link with the deCONZ bridge."""
|
||||
from pydeconz.utils import async_get_api_key
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
api_key = await async_get_api_key(session, **self.deconz_config)
|
||||
if api_key:
|
||||
self.deconz_config[CONF_API_KEY] = api_key
|
||||
return self.async_create_entry(
|
||||
title='deCONZ',
|
||||
data=self.deconz_config
|
||||
)
|
||||
else:
|
||||
errors['base'] = 'no_key'
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='link',
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up a bridge for a config entry."""
|
||||
if DOMAIN in hass.data:
|
||||
_LOGGER.error(
|
||||
"Config entry failed since one deCONZ instance already exists")
|
||||
return False
|
||||
result = await async_setup_deconz(hass, None, entry.data)
|
||||
if result:
|
||||
return True
|
||||
return False
|
||||
|
|
139
homeassistant/components/deconz/config_flow.py
Normal file
139
homeassistant/components/deconz/config_flow.py
Normal file
|
@ -0,0 +1,139 @@
|
|||
"""Config flow to configure deCONZ component."""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
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 CONFIG_FILE, DOMAIN
|
||||
|
||||
|
||||
@callback
|
||||
def configured_hosts(hass):
|
||||
"""Return a set of the configured hosts."""
|
||||
return set(entry.data['host'] for entry
|
||||
in hass.config_entries.async_entries(DOMAIN))
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class DeconzFlowHandler(data_entry_flow.FlowHandler):
|
||||
"""Handle a deCONZ config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the deCONZ config flow."""
|
||||
self.bridges = []
|
||||
self.deconz_config = {}
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle a deCONZ config flow start."""
|
||||
from pydeconz.utils import async_discovery
|
||||
|
||||
if configured_hosts(self.hass):
|
||||
return self.async_abort(reason='one_instance_only')
|
||||
|
||||
if user_input is not None:
|
||||
for bridge in self.bridges:
|
||||
if bridge[CONF_HOST] == user_input[CONF_HOST]:
|
||||
self.deconz_config = bridge
|
||||
return await self.async_step_link()
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
self.bridges = await async_discovery(session)
|
||||
|
||||
if len(self.bridges) == 1:
|
||||
self.deconz_config = self.bridges[0]
|
||||
return await self.async_step_link()
|
||||
elif len(self.bridges) > 1:
|
||||
hosts = []
|
||||
for bridge in self.bridges:
|
||||
hosts.append(bridge[CONF_HOST])
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_HOST): vol.In(hosts)
|
||||
})
|
||||
)
|
||||
|
||||
return self.async_abort(
|
||||
reason='no_bridges'
|
||||
)
|
||||
|
||||
async def async_step_link(self, user_input=None):
|
||||
"""Attempt to link with the deCONZ bridge."""
|
||||
from pydeconz.utils import async_get_api_key, async_get_bridgeid
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
if configured_hosts(self.hass):
|
||||
return self.async_abort(reason='one_instance_only')
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
api_key = await async_get_api_key(session, **self.deconz_config)
|
||||
if api_key:
|
||||
self.deconz_config[CONF_API_KEY] = api_key
|
||||
if 'bridgeid' not in self.deconz_config:
|
||||
self.deconz_config['bridgeid'] = await async_get_bridgeid(
|
||||
session, **self.deconz_config)
|
||||
return self.async_create_entry(
|
||||
title='deCONZ-' + self.deconz_config['bridgeid'],
|
||||
data=self.deconz_config
|
||||
)
|
||||
errors['base'] = 'no_key'
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='link',
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
"""Prepare configuration for a discovered deCONZ bridge.
|
||||
|
||||
This flow is triggered by the discovery component.
|
||||
"""
|
||||
deconz_config = {}
|
||||
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
|
||||
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
|
||||
deconz_config['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):
|
||||
"""Import a deCONZ bridge as a config entry.
|
||||
|
||||
This flow is triggered by `async_setup` for configured bridges.
|
||||
This flow is also triggered by `async_step_discovery`.
|
||||
|
||||
This will execute for any bridge that does not have a
|
||||
config entry yet (based on host).
|
||||
|
||||
If an API key is provided, we will create an entry.
|
||||
Otherwise we will delegate to `link` step which
|
||||
will ask user to link the bridge.
|
||||
"""
|
||||
from pydeconz.utils import async_get_bridgeid
|
||||
|
||||
if configured_hosts(self.hass):
|
||||
return self.async_abort(reason='one_instance_only')
|
||||
elif CONF_API_KEY not in import_config:
|
||||
self.deconz_config = import_config
|
||||
return await self.async_step_link()
|
||||
|
||||
if 'bridgeid' not in import_config:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
import_config['bridgeid'] = await async_get_bridgeid(
|
||||
session, **import_config)
|
||||
return self.async_create_entry(
|
||||
title='deCONZ-' + import_config['bridgeid'],
|
||||
data=import_config
|
||||
)
|
8
homeassistant/components/deconz/const.py
Normal file
8
homeassistant/components/deconz/const.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
"""Constants for the deCONZ component."""
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger('homeassistant.components.deconz')
|
||||
|
||||
DOMAIN = 'deconz'
|
||||
CONFIG_FILE = 'deconz.conf'
|
||||
DATA_DECONZ_ID = 'deconz_entities'
|
|
@ -18,6 +18,7 @@
|
|||
"no_key": "Couldn't get an API key"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Bridge is already configured",
|
||||
"no_bridges": "No deCONZ bridges discovered",
|
||||
"one_instance_only": "Component only supports one deCONZ instance"
|
||||
}
|
||||
|
|
|
@ -605,6 +605,17 @@ class DeviceScanner(object):
|
|||
"""
|
||||
return self.hass.async_add_job(self.get_device_name, device)
|
||||
|
||||
def get_extra_attributes(self, device: str) -> dict:
|
||||
"""Get the extra attributes of a device."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_get_extra_attributes(self, device: str) -> Any:
|
||||
"""Get the extra attributes of a device.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.get_extra_attributes, device)
|
||||
|
||||
|
||||
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
"""Load devices from YAML configuration file."""
|
||||
|
@ -690,10 +701,20 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
|||
host_name = yield from scanner.async_get_device_name(mac)
|
||||
seen.add(mac)
|
||||
|
||||
try:
|
||||
extra_attributes = (yield from
|
||||
scanner.async_get_extra_attributes(mac))
|
||||
except NotImplementedError:
|
||||
extra_attributes = dict()
|
||||
|
||||
kwargs = {
|
||||
'mac': mac,
|
||||
'host_name': host_name,
|
||||
'source_type': SOURCE_TYPE_ROUTER
|
||||
'source_type': SOURCE_TYPE_ROUTER,
|
||||
'attributes': {
|
||||
'scanner': scanner.__class__.__name__,
|
||||
**extra_attributes
|
||||
}
|
||||
}
|
||||
|
||||
zone_home = hass.states.get(zone.ENTITY_ID_HOME)
|
||||
|
|
|
@ -48,8 +48,11 @@ class BMWDeviceTracker(object):
|
|||
return
|
||||
|
||||
_LOGGER.debug('Updating %s', dev_id)
|
||||
|
||||
attrs = {
|
||||
'vin': self.vehicle.vin,
|
||||
}
|
||||
self._see(
|
||||
dev_id=dev_id, host_name=self.vehicle.name,
|
||||
gps=self.vehicle.state.gps_position, icon='mdi:car'
|
||||
gps=self.vehicle.state.gps_position, attributes=attrs,
|
||||
icon='mdi:car'
|
||||
)
|
||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.util import slugify
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['locationsharinglib==0.4.0']
|
||||
REQUIREMENTS = ['locationsharinglib==1.2.1']
|
||||
|
||||
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
|
||||
|
||||
|
|
|
@ -176,7 +176,7 @@ class MikrotikScanner(DeviceScanner):
|
|||
for device in device_names
|
||||
if device.get('mac-address')}
|
||||
|
||||
if self.wireless_exist:
|
||||
if self.wireless_exist or self.capsman_exist:
|
||||
self.last_results = {
|
||||
device.get('mac-address'):
|
||||
mac_names.get(device.get('mac-address'))
|
||||
|
|
|
@ -80,6 +80,8 @@ class NmapDeviceScanner(DeviceScanner):
|
|||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
_LOGGER.debug("Nmap last results %s", self.last_results)
|
||||
|
||||
return [device.mac for device in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
|
@ -91,6 +93,13 @@ class NmapDeviceScanner(DeviceScanner):
|
|||
return filter_named[0]
|
||||
return None
|
||||
|
||||
def get_extra_attributes(self, device):
|
||||
"""Return the IP of the given device."""
|
||||
filter_ip = next((
|
||||
result.ip for result in self.last_results
|
||||
if result.mac == device), None)
|
||||
return {'ip': filter_ip}
|
||||
|
||||
def _update_info(self):
|
||||
"""Scan the network for devices.
|
||||
|
||||
|
|
|
@ -103,6 +103,9 @@ class UbusDeviceScanner(DeviceScanner):
|
|||
"""Return the name of the given device or None if we don't know."""
|
||||
if self.mac2name is None:
|
||||
self._generate_mac2name()
|
||||
if self.mac2name is None:
|
||||
# Generation of mac2name dictionary failed
|
||||
return None
|
||||
name = self.mac2name.get(device.upper(), None)
|
||||
return name
|
||||
|
||||
|
|
|
@ -122,3 +122,9 @@ class UnifiScanner(DeviceScanner):
|
|||
name = client.get('name') or client.get('hostname')
|
||||
_LOGGER.debug("Device mac %s name %s", device, name)
|
||||
return name
|
||||
|
||||
def get_extra_attributes(self, device):
|
||||
"""Return the extra attributes of the device."""
|
||||
client = self._clients.get(device, {})
|
||||
_LOGGER.debug("Device mac %s attributes %s", device, client)
|
||||
return client
|
||||
|
|
|
@ -20,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.9']
|
||||
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
|
@ -41,7 +41,7 @@ def get_scanner(hass, config):
|
|||
device_info.model,
|
||||
device_info.firmware_version,
|
||||
device_info.hardware_version)
|
||||
scanner = XiaomiMiioDeviceScanner(hass, device)
|
||||
scanner = XiaomiMiioDeviceScanner(device)
|
||||
except DeviceException as ex:
|
||||
_LOGGER.error("Device unavailable or token incorrect: %s", ex)
|
||||
|
||||
|
@ -51,7 +51,7 @@ def get_scanner(hass, config):
|
|||
class XiaomiMiioDeviceScanner(DeviceScanner):
|
||||
"""This class queries a Xiaomi Mi WiFi Repeater."""
|
||||
|
||||
def __init__(self, hass, device):
|
||||
def __init__(self, device):
|
||||
"""Initialize the scanner."""
|
||||
self.device = device
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import os
|
|||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
|
|||
from homeassistant.helpers.discovery import async_load_platform, async_discover
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['netdisco==1.3.0']
|
||||
REQUIREMENTS = ['netdisco==1.3.1']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
|
@ -40,8 +40,10 @@ SERVICE_HUE = 'philips_hue'
|
|||
SERVICE_DECONZ = 'deconz'
|
||||
SERVICE_DAIKIN = 'daikin'
|
||||
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
|
||||
SERVICE_HOMEKIT = 'homekit'
|
||||
|
||||
CONFIG_ENTRY_HANDLERS = {
|
||||
SERVICE_DECONZ: 'deconz',
|
||||
SERVICE_HUE: 'hue',
|
||||
}
|
||||
|
||||
|
@ -56,7 +58,6 @@ SERVICE_HANDLERS = {
|
|||
SERVICE_WINK: ('wink', None),
|
||||
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
|
||||
SERVICE_TELLDUSLIVE: ('tellduslive', None),
|
||||
SERVICE_DECONZ: ('deconz', None),
|
||||
SERVICE_DAIKIN: ('daikin', None),
|
||||
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
|
@ -77,15 +78,23 @@ SERVICE_HANDLERS = {
|
|||
'bose_soundtouch': ('media_player', 'soundtouch'),
|
||||
'bluesound': ('media_player', 'bluesound'),
|
||||
'songpal': ('media_player', 'songpal'),
|
||||
'kodi': ('media_player', 'kodi'),
|
||||
}
|
||||
|
||||
OPTIONAL_SERVICE_HANDLERS = {
|
||||
SERVICE_HOMEKIT: ('homekit_controller', None),
|
||||
}
|
||||
|
||||
CONF_IGNORE = 'ignore'
|
||||
CONF_ENABLE = 'enable'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(DOMAIN): vol.Schema({
|
||||
vol.Optional(CONF_IGNORE, default=[]):
|
||||
vol.All(cv.ensure_list, [
|
||||
vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))])
|
||||
vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))]),
|
||||
vol.Optional(CONF_ENABLE, default=[]):
|
||||
vol.All(cv.ensure_list, [vol.In(OPTIONAL_SERVICE_HANDLERS)])
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
@ -104,6 +113,9 @@ async def async_setup(hass, config):
|
|||
# Platforms ignore by config
|
||||
ignored_platforms = config[DOMAIN][CONF_IGNORE]
|
||||
|
||||
# Optional platforms enabled by config
|
||||
enabled_platforms = config[DOMAIN][CONF_ENABLE]
|
||||
|
||||
async def new_service_found(service, info):
|
||||
"""Handle a new service if one is found."""
|
||||
if service in ignored_platforms:
|
||||
|
@ -119,13 +131,16 @@ async def async_setup(hass, config):
|
|||
if service in CONFIG_ENTRY_HANDLERS:
|
||||
await hass.config_entries.flow.async_init(
|
||||
CONFIG_ENTRY_HANDLERS[service],
|
||||
source=config_entries.SOURCE_DISCOVERY,
|
||||
source=data_entry_flow.SOURCE_DISCOVERY,
|
||||
data=info
|
||||
)
|
||||
return
|
||||
|
||||
comp_plat = SERVICE_HANDLERS.get(service)
|
||||
|
||||
if not comp_plat and service in enabled_platforms:
|
||||
comp_plat = OPTIONAL_SERVICE_HANDLERS[service]
|
||||
|
||||
# We do not know how to handle this service.
|
||||
if not comp_plat:
|
||||
logger.info("Unknown service discovered: %s %s", service, info)
|
||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.const import CONF_API_KEY
|
|||
from homeassistant.util import Throttle
|
||||
from homeassistant.util.json import save_json
|
||||
|
||||
REQUIREMENTS = ['python-ecobee-api==0.0.17']
|
||||
REQUIREMENTS = ['python-ecobee-api==0.0.18']
|
||||
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
|
77
homeassistant/components/eufy.py
Normal file
77
homeassistant/components/eufy.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
"""
|
||||
Support for Eufy devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/eufy/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, \
|
||||
CONF_DEVICES, CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, CONF_NAME
|
||||
from homeassistant.helpers import discovery
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
REQUIREMENTS = ['lakeside==0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'eufy'
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ADDRESS): cv.string,
|
||||
vol.Required(CONF_ACCESS_TOKEN): cv.string,
|
||||
vol.Required(CONF_TYPE): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_DEVICES, default=[]): vol.All(cv.ensure_list,
|
||||
[DEVICE_SCHEMA]),
|
||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
EUFY_DISPATCH = {
|
||||
'T1011': 'light',
|
||||
'T1012': 'light',
|
||||
'T1013': 'light',
|
||||
'T1201': 'switch',
|
||||
'T1202': 'switch',
|
||||
'T1211': 'switch'
|
||||
}
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up Eufy devices."""
|
||||
# pylint: disable=import-error
|
||||
import lakeside
|
||||
|
||||
if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]:
|
||||
data = lakeside.get_devices(config[DOMAIN][CONF_USERNAME],
|
||||
config[DOMAIN][CONF_PASSWORD])
|
||||
for device in data:
|
||||
kind = device['type']
|
||||
if kind not in EUFY_DISPATCH:
|
||||
continue
|
||||
discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device,
|
||||
config)
|
||||
|
||||
for device_info in config[DOMAIN][CONF_DEVICES]:
|
||||
kind = device_info['type']
|
||||
if kind not in EUFY_DISPATCH:
|
||||
continue
|
||||
device = {}
|
||||
device['address'] = device_info['address']
|
||||
device['code'] = device_info['access_token']
|
||||
device['type'] = device_info['type']
|
||||
device['name'] = device_info['name']
|
||||
discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device,
|
||||
config)
|
||||
|
||||
return True
|
83
homeassistant/components/fritzbox.py
Executable file
83
homeassistant/components/fritzbox.py
Executable file
|
@ -0,0 +1,83 @@
|
|||
"""
|
||||
Support for AVM Fritz!Box smarthome devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
http://home-assistant.io/components/fritzbox/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import discovery
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pyfritzhome==0.3.7']
|
||||
|
||||
SUPPORTED_DOMAINS = ['climate', 'switch']
|
||||
|
||||
DOMAIN = 'fritzbox'
|
||||
|
||||
ATTR_STATE_DEVICE_LOCKED = 'device_locked'
|
||||
ATTR_STATE_LOCKED = 'locked'
|
||||
ATTR_STATE_BATTERY_LOW = 'battery_low'
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_DEVICES):
|
||||
vol.All(cv.ensure_list, [
|
||||
vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the fritzbox component."""
|
||||
from pyfritzhome import Fritzhome, LoginError
|
||||
|
||||
fritz_list = []
|
||||
|
||||
configured_devices = config[DOMAIN].get(CONF_DEVICES)
|
||||
for device in configured_devices:
|
||||
host = device.get(CONF_HOST)
|
||||
username = device.get(CONF_USERNAME)
|
||||
password = device.get(CONF_PASSWORD)
|
||||
fritzbox = Fritzhome(host=host, user=username,
|
||||
password=password)
|
||||
try:
|
||||
fritzbox.login()
|
||||
_LOGGER.info("Connected to device %s", device)
|
||||
except LoginError:
|
||||
_LOGGER.warning("Login to Fritz!Box %s as %s failed",
|
||||
host, username)
|
||||
continue
|
||||
|
||||
fritz_list.append(fritzbox)
|
||||
|
||||
if not fritz_list:
|
||||
_LOGGER.info("No fritzboxes configured")
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN] = fritz_list
|
||||
|
||||
def logout_fritzboxes(event):
|
||||
"""Close all connections to the fritzboxes."""
|
||||
for fritz in fritz_list:
|
||||
fritz.logout()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzboxes)
|
||||
|
||||
for domain in SUPPORTED_DOMAINS:
|
||||
discovery.load_platform(hass, domain, DOMAIN, {}, config)
|
||||
|
||||
return True
|
|
@ -24,7 +24,7 @@ from homeassistant.core import callback
|
|||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180404.0']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180420.0']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
||||
|
|
|
@ -44,6 +44,7 @@ CONF_ENTITIES = 'entities'
|
|||
CONF_TRACK = 'track'
|
||||
CONF_SEARCH = 'search'
|
||||
CONF_OFFSET = 'offset'
|
||||
CONF_IGNORE_AVAILABILITY = 'ignore_availability'
|
||||
|
||||
DEFAULT_CONF_TRACK_NEW = True
|
||||
DEFAULT_CONF_OFFSET = '!!'
|
||||
|
@ -74,8 +75,9 @@ _SINGLE_CALSEARCH_CONFIG = vol.Schema({
|
|||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_DEVICE_ID): cv.string,
|
||||
vol.Optional(CONF_TRACK): cv.boolean,
|
||||
vol.Optional(CONF_SEARCH): vol.Any(cv.string, None),
|
||||
vol.Optional(CONF_SEARCH): cv.string,
|
||||
vol.Optional(CONF_OFFSET): cv.string,
|
||||
vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean,
|
||||
})
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
|
|
|
@ -35,7 +35,7 @@ CONF_TYPES = 'types'
|
|||
ICON_UNKNOWN = 'mdi:help'
|
||||
ICON_AUDIO = 'mdi:speaker'
|
||||
ICON_PLAYER = 'mdi:play'
|
||||
ICON_TUNER = 'mdi:nest-thermostat'
|
||||
ICON_TUNER = 'mdi:radio'
|
||||
ICON_RECORDER = 'mdi:microphone'
|
||||
ICON_TV = 'mdi:television'
|
||||
ICONS_BY_TYPE = {
|
||||
|
|
|
@ -118,6 +118,30 @@ def state_changes_during_period(hass, start_time, end_time=None,
|
|||
return states_to_json(hass, states, start_time, entity_ids)
|
||||
|
||||
|
||||
def get_last_state_changes(hass, number_of_states, entity_id):
|
||||
"""Return the last number_of_states."""
|
||||
from homeassistant.components.recorder.models import States
|
||||
|
||||
start_time = dt_util.utcnow()
|
||||
|
||||
with session_scope(hass=hass) as session:
|
||||
query = session.query(States).filter(
|
||||
(States.last_changed == States.last_updated))
|
||||
|
||||
if entity_id is not None:
|
||||
query = query.filter_by(entity_id=entity_id.lower())
|
||||
|
||||
entity_ids = [entity_id] if entity_id is not None else None
|
||||
|
||||
states = execute(
|
||||
query.order_by(States.last_updated.desc()).limit(number_of_states))
|
||||
|
||||
return states_to_json(hass, reversed(states),
|
||||
start_time,
|
||||
entity_ids,
|
||||
include_start_time_state=False)
|
||||
|
||||
|
||||
def get_states(hass, utc_point_in_time, entity_ids=None, run=None,
|
||||
filters=None):
|
||||
"""Return the states at a specific point in time."""
|
||||
|
|
|
@ -12,7 +12,7 @@ from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL,
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
|
||||
REQUIREMENTS = ['pyhiveapi==0.2.11']
|
||||
REQUIREMENTS = ['pyhiveapi==0.2.14']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'hive'
|
||||
|
@ -44,6 +44,8 @@ class HiveSession:
|
|||
light = None
|
||||
sensor = None
|
||||
switch = None
|
||||
weather = None
|
||||
attributes = None
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
@ -70,6 +72,8 @@ def setup(hass, config):
|
|||
session.hotwater = Pyhiveapi.Hotwater()
|
||||
session.light = Pyhiveapi.Light()
|
||||
session.switch = Pyhiveapi.Switch()
|
||||
session.weather = Pyhiveapi.Weather()
|
||||
session.attributes = Pyhiveapi.Attributes()
|
||||
hass.data[DATA_HIVE] = session
|
||||
|
||||
for ha_type, hive_type in DEVICETYPES.items():
|
||||
|
|
|
@ -8,12 +8,11 @@ from zlib import adler32
|
|||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
from homeassistant.components.cover import SUPPORT_SET_POSITION
|
||||
from homeassistant.components.cover import (
|
||||
SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION)
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
||||
ATTR_DEVICE_CLASS, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
|
||||
|
@ -21,14 +20,16 @@ from homeassistant.util import get_local_ip
|
|||
from homeassistant.util.decorator import Registry
|
||||
from .const import (
|
||||
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER,
|
||||
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START)
|
||||
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START,
|
||||
DEVICE_CLASS_CO2, DEVICE_CLASS_LIGHT, DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PM25, DEVICE_CLASS_TEMPERATURE)
|
||||
from .util import (
|
||||
validate_entity_config, show_setup_message)
|
||||
|
||||
TYPES = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['HAP-python==1.1.8']
|
||||
REQUIREMENTS = ['HAP-python==1.1.9']
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
|
@ -79,55 +80,64 @@ def get_accessory(hass, state, aid, config):
|
|||
state.entity_id)
|
||||
return None
|
||||
|
||||
if state.domain == 'sensor':
|
||||
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT:
|
||||
_LOGGER.debug('Add "%s" as "%s"',
|
||||
state.entity_id, 'TemperatureSensor')
|
||||
return TYPES['TemperatureSensor'](hass, state.entity_id,
|
||||
state.name, aid=aid)
|
||||
elif unit == '%':
|
||||
_LOGGER.debug('Add "%s" as %s"',
|
||||
state.entity_id, 'HumiditySensor')
|
||||
return TYPES['HumiditySensor'](hass, state.entity_id, state.name,
|
||||
aid=aid)
|
||||
a_type = None
|
||||
config = config or {}
|
||||
|
||||
elif state.domain == 'cover':
|
||||
# Only add covers that support set_cover_position
|
||||
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if features & SUPPORT_SET_POSITION:
|
||||
_LOGGER.debug('Add "%s" as "%s"',
|
||||
state.entity_id, 'WindowCovering')
|
||||
return TYPES['WindowCovering'](hass, state.entity_id, state.name,
|
||||
aid=aid)
|
||||
if state.domain == 'alarm_control_panel':
|
||||
a_type = 'SecuritySystem'
|
||||
|
||||
elif state.domain == 'alarm_control_panel':
|
||||
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem')
|
||||
return TYPES['SecuritySystem'](hass, state.entity_id, state.name,
|
||||
alarm_code=config.get(ATTR_CODE),
|
||||
aid=aid)
|
||||
elif state.domain == 'binary_sensor' or state.domain == 'device_tracker':
|
||||
a_type = 'BinarySensor'
|
||||
|
||||
elif state.domain == 'climate':
|
||||
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH
|
||||
# Check if climate device supports auto mode
|
||||
support_auto = bool(features & support_temp_range)
|
||||
a_type = 'Thermostat'
|
||||
|
||||
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat')
|
||||
return TYPES['Thermostat'](hass, state.entity_id,
|
||||
state.name, support_auto, aid=aid)
|
||||
elif state.domain == 'cover':
|
||||
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
|
||||
if device_class == 'garage' and \
|
||||
features & (SUPPORT_OPEN | SUPPORT_CLOSE):
|
||||
a_type = 'GarageDoorOpener'
|
||||
elif features & SUPPORT_SET_POSITION:
|
||||
a_type = 'WindowCovering'
|
||||
elif features & (SUPPORT_OPEN | SUPPORT_CLOSE):
|
||||
a_type = 'WindowCoveringBasic'
|
||||
|
||||
elif state.domain == 'light':
|
||||
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light')
|
||||
return TYPES['Light'](hass, state.entity_id, state.name, aid=aid)
|
||||
a_type = 'Light'
|
||||
|
||||
elif state.domain == 'lock':
|
||||
a_type = 'Lock'
|
||||
|
||||
elif state.domain == 'sensor':
|
||||
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
|
||||
if device_class == DEVICE_CLASS_TEMPERATURE or unit == TEMP_CELSIUS \
|
||||
or unit == TEMP_FAHRENHEIT:
|
||||
a_type = 'TemperatureSensor'
|
||||
elif device_class == DEVICE_CLASS_HUMIDITY or unit == '%':
|
||||
a_type = 'HumiditySensor'
|
||||
elif device_class == DEVICE_CLASS_PM25 \
|
||||
or DEVICE_CLASS_PM25 in state.entity_id:
|
||||
a_type = 'AirQualitySensor'
|
||||
elif device_class == DEVICE_CLASS_CO2 \
|
||||
or DEVICE_CLASS_CO2 in state.entity_id:
|
||||
a_type = 'CarbonDioxideSensor'
|
||||
elif device_class == DEVICE_CLASS_LIGHT or unit == 'lm' or \
|
||||
unit == 'lux':
|
||||
a_type = 'LightSensor'
|
||||
|
||||
elif state.domain == 'switch' or state.domain == 'remote' \
|
||||
or state.domain == 'input_boolean' or state.domain == 'script':
|
||||
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch')
|
||||
return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid)
|
||||
a_type = 'Switch'
|
||||
|
||||
return None
|
||||
if a_type is None:
|
||||
return None
|
||||
|
||||
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type)
|
||||
return TYPES[a_type](hass, state.name, state.entity_id, aid, config=config)
|
||||
|
||||
|
||||
def generate_aid(entity_id):
|
||||
|
@ -143,7 +153,7 @@ class HomeKit():
|
|||
|
||||
def __init__(self, hass, port, entity_filter, entity_config):
|
||||
"""Initialize a HomeKit object."""
|
||||
self._hass = hass
|
||||
self.hass = hass
|
||||
self._port = port
|
||||
self._filter = entity_filter
|
||||
self._config = entity_config
|
||||
|
@ -156,11 +166,11 @@ class HomeKit():
|
|||
"""Setup bridge and accessory driver."""
|
||||
from .accessories import HomeBridge, HomeDriver
|
||||
|
||||
self._hass.bus.async_listen_once(
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, self.stop)
|
||||
|
||||
path = self._hass.config.path(HOMEKIT_FILE)
|
||||
self.bridge = HomeBridge(self._hass)
|
||||
path = self.hass.config.path(HOMEKIT_FILE)
|
||||
self.bridge = HomeBridge(self.hass)
|
||||
self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path)
|
||||
|
||||
def add_bridge_accessory(self, state):
|
||||
|
@ -169,7 +179,7 @@ class HomeKit():
|
|||
return
|
||||
aid = generate_aid(state.entity_id)
|
||||
conf = self._config.pop(state.entity_id, {})
|
||||
acc = get_accessory(self._hass, state, aid, conf)
|
||||
acc = get_accessory(self.hass, state, aid, conf)
|
||||
if acc is not None:
|
||||
self.bridge.add_accessory(acc)
|
||||
|
||||
|
@ -181,15 +191,15 @@ class HomeKit():
|
|||
|
||||
# pylint: disable=unused-variable
|
||||
from . import ( # noqa F401
|
||||
type_covers, type_lights, type_security_systems, type_sensors,
|
||||
type_switches, type_thermostats)
|
||||
type_covers, type_lights, type_locks, type_security_systems,
|
||||
type_sensors, type_switches, type_thermostats)
|
||||
|
||||
for state in self._hass.states.all():
|
||||
for state in self.hass.states.all():
|
||||
self.add_bridge_accessory(state)
|
||||
self.bridge.set_broker(self.driver)
|
||||
|
||||
if not self.bridge.paired:
|
||||
show_setup_message(self.bridge, self._hass)
|
||||
show_setup_message(self.hass, self.bridge)
|
||||
|
||||
_LOGGER.debug('Driver start')
|
||||
self.driver.start()
|
||||
|
|
|
@ -1,21 +1,64 @@
|
|||
"""Extend the basic Accessory and Bridge functions."""
|
||||
from datetime import timedelta
|
||||
from functools import wraps
|
||||
from inspect import getmodule
|
||||
import logging
|
||||
|
||||
from pyhap.accessory import Accessory, Bridge, Category
|
||||
from pyhap.accessory_driver import AccessoryDriver
|
||||
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.core import callback as ha_callback
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, track_point_in_utc_time)
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
|
||||
MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL,
|
||||
CHAR_NAME, CHAR_SERIAL_NUMBER)
|
||||
DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER,
|
||||
SERV_ACCESSORY_INFO, CHAR_MANUFACTURER,
|
||||
CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
|
||||
from .util import (
|
||||
show_setup_message, dismiss_setup_message)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def debounce(func):
|
||||
"""Decorator function. Debounce callbacks form HomeKit."""
|
||||
@ha_callback
|
||||
def call_later_listener(*args):
|
||||
"""Callback listener called from call_later."""
|
||||
# pylint: disable=unsubscriptable-object
|
||||
nonlocal lastargs, remove_listener
|
||||
hass = lastargs['hass']
|
||||
hass.async_add_job(func, *lastargs['args'])
|
||||
lastargs = remove_listener = None
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args):
|
||||
"""Wrapper starts async timer.
|
||||
|
||||
The accessory must have 'self.hass' and 'self.entity_id' as attributes.
|
||||
"""
|
||||
# pylint: disable=not-callable
|
||||
hass = args[0].hass
|
||||
nonlocal lastargs, remove_listener
|
||||
if remove_listener:
|
||||
remove_listener()
|
||||
lastargs = remove_listener = None
|
||||
lastargs = {'hass': hass, 'args': [*args]}
|
||||
remove_listener = track_point_in_utc_time(
|
||||
hass, call_later_listener,
|
||||
dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT))
|
||||
logger.debug('%s: Start %s timeout', args[0].entity_id,
|
||||
func.__name__.replace('set_', ''))
|
||||
|
||||
remove_listener = None
|
||||
lastargs = None
|
||||
name = getmodule(func).__name__
|
||||
logger = logging.getLogger(name)
|
||||
return wrapper
|
||||
|
||||
|
||||
def add_preload_service(acc, service, chars=None):
|
||||
"""Define and return a service to be available for the accessory."""
|
||||
from pyhap.loader import get_serv_loader, get_char_loader
|
||||
|
@ -29,6 +72,18 @@ def add_preload_service(acc, service, chars=None):
|
|||
return service
|
||||
|
||||
|
||||
def setup_char(char_name, service, value=None, properties=None, callback=None):
|
||||
"""Helper function to return fully configured characteristic."""
|
||||
char = service.get_characteristic(char_name)
|
||||
if value:
|
||||
char.value = value
|
||||
if properties:
|
||||
char.override_properties(properties)
|
||||
if callback:
|
||||
char.setter_callback = callback
|
||||
return char
|
||||
|
||||
|
||||
def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
|
||||
serial_number='0000'):
|
||||
"""Set the default accessory information."""
|
||||
|
@ -42,14 +97,13 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
|
|||
class HomeAccessory(Accessory):
|
||||
"""Adapter class for Accessory."""
|
||||
|
||||
# pylint: disable=no-member
|
||||
|
||||
def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL,
|
||||
category='OTHER', **kwargs):
|
||||
def __init__(self, hass, name, entity_id, aid, category):
|
||||
"""Initialize a Accessory object."""
|
||||
super().__init__(name, **kwargs)
|
||||
set_accessory_info(self, name, model)
|
||||
super().__init__(name, aid=aid)
|
||||
set_accessory_info(self, name, model=entity_id)
|
||||
self.category = getattr(Category, category, Category.OTHER)
|
||||
self.entity_id = entity_id
|
||||
self.hass = hass
|
||||
|
||||
def _set_services(self):
|
||||
add_preload_service(self, SERV_ACCESSORY_INFO)
|
||||
|
@ -57,19 +111,33 @@ class HomeAccessory(Accessory):
|
|||
def run(self):
|
||||
"""Method called by accessory after driver is started."""
|
||||
state = self.hass.states.get(self.entity_id)
|
||||
self.update_state(new_state=state)
|
||||
self.update_state_callback(new_state=state)
|
||||
async_track_state_change(
|
||||
self.hass, self.entity_id, self.update_state)
|
||||
self.hass, self.entity_id, self.update_state_callback)
|
||||
|
||||
def update_state_callback(self, entity_id=None, old_state=None,
|
||||
new_state=None):
|
||||
"""Callback from state change listener."""
|
||||
_LOGGER.debug('New_state: %s', new_state)
|
||||
if new_state is None:
|
||||
return
|
||||
self.update_state(new_state)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Method called on state change to update HomeKit value.
|
||||
|
||||
Overridden by accessory types.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class HomeBridge(Bridge):
|
||||
"""Adapter class for Bridge."""
|
||||
|
||||
def __init__(self, hass, name=BRIDGE_NAME,
|
||||
model=BRIDGE_MODEL, **kwargs):
|
||||
def __init__(self, hass, name=BRIDGE_NAME):
|
||||
"""Initialize a Bridge object."""
|
||||
super().__init__(name, **kwargs)
|
||||
set_accessory_info(self, name, model)
|
||||
super().__init__(name)
|
||||
set_accessory_info(self, name, model=BRIDGE_MODEL)
|
||||
self.hass = hass
|
||||
|
||||
def _set_services(self):
|
||||
|
@ -87,7 +155,7 @@ class HomeBridge(Bridge):
|
|||
def remove_paired_client(self, client_uuid):
|
||||
"""Override super function to show setup message if unpaired."""
|
||||
super().remove_paired_client(client_uuid)
|
||||
show_setup_message(self, self.hass)
|
||||
show_setup_message(self.hass, self)
|
||||
|
||||
|
||||
class HomeDriver(AccessoryDriver):
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Constants used be the HomeKit component."""
|
||||
# #### MISC ####
|
||||
DEBOUNCE_TIMEOUT = 0.5
|
||||
DOMAIN = 'homekit'
|
||||
HOMEKIT_FILE = '.homekit.state'
|
||||
HOMEKIT_NOTIFY_ID = 4663548
|
||||
|
@ -17,15 +18,15 @@ DEFAULT_PORT = 51827
|
|||
SERVICE_HOMEKIT_START = 'start'
|
||||
|
||||
# #### STRING CONSTANTS ####
|
||||
ACCESSORY_MODEL = 'homekit.accessory'
|
||||
ACCESSORY_NAME = 'Home Accessory'
|
||||
BRIDGE_MODEL = 'homekit.bridge'
|
||||
BRIDGE_NAME = 'Home Assistant'
|
||||
MANUFACTURER = 'HomeAssistant'
|
||||
|
||||
# #### Categories ####
|
||||
CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM'
|
||||
CATEGORY_GARAGE_DOOR_OPENER = 'GARAGE_DOOR_OPENER'
|
||||
CATEGORY_LIGHT = 'LIGHTBULB'
|
||||
CATEGORY_LOCK = 'DOOR_LOCK'
|
||||
CATEGORY_SENSOR = 'SENSOR'
|
||||
CATEGORY_SWITCH = 'SWITCH'
|
||||
CATEGORY_THERMOSTAT = 'THERMOSTAT'
|
||||
|
@ -34,40 +35,80 @@ CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING'
|
|||
|
||||
# #### Services ####
|
||||
SERV_ACCESSORY_INFO = 'AccessoryInformation'
|
||||
SERV_HUMIDITY_SENSOR = 'HumiditySensor'
|
||||
# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered,
|
||||
# StatusLowBattery, Name
|
||||
SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor'
|
||||
SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor'
|
||||
SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor'
|
||||
SERV_CONTACT_SENSOR = 'ContactSensor'
|
||||
SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener'
|
||||
SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity
|
||||
SERV_LEAK_SENSOR = 'LeakSensor'
|
||||
SERV_LIGHT_SENSOR = 'LightSensor'
|
||||
SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name
|
||||
SERV_LOCK = 'LockMechanism'
|
||||
SERV_MOTION_SENSOR = 'MotionSensor'
|
||||
SERV_OCCUPANCY_SENSOR = 'OccupancySensor'
|
||||
SERV_SECURITY_SYSTEM = 'SecuritySystem'
|
||||
SERV_SMOKE_SENSOR = 'SmokeSensor'
|
||||
SERV_SWITCH = 'Switch'
|
||||
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
|
||||
SERV_THERMOSTAT = 'Thermostat'
|
||||
SERV_WINDOW_COVERING = 'WindowCovering'
|
||||
# CurrentPosition, TargetPosition, PositionState
|
||||
|
||||
|
||||
# #### Characteristics ####
|
||||
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
|
||||
CHAR_AIR_QUALITY = 'AirQuality'
|
||||
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100]
|
||||
CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected'
|
||||
CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel'
|
||||
CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel'
|
||||
CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected'
|
||||
CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
|
||||
CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState'
|
||||
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
|
||||
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel'
|
||||
CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState'
|
||||
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
|
||||
CHAR_CURRENT_POSITION = 'CurrentPosition'
|
||||
CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100]
|
||||
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
|
||||
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
|
||||
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
|
||||
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
|
||||
CHAR_LEAK_DETECTED = 'LeakDetected'
|
||||
CHAR_LOCK_CURRENT_STATE = 'LockCurrentState'
|
||||
CHAR_LOCK_TARGET_STATE = 'LockTargetState'
|
||||
CHAR_LINK_QUALITY = 'LinkQuality'
|
||||
CHAR_MANUFACTURER = 'Manufacturer'
|
||||
CHAR_MODEL = 'Model'
|
||||
CHAR_MOTION_DETECTED = 'MotionDetected'
|
||||
CHAR_NAME = 'Name'
|
||||
CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected'
|
||||
CHAR_ON = 'On' # boolean
|
||||
CHAR_POSITION_STATE = 'PositionState'
|
||||
CHAR_SATURATION = 'Saturation' # percent
|
||||
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
||||
CHAR_SMOKE_DETECTED = 'SmokeDetected'
|
||||
CHAR_TARGET_DOOR_STATE = 'TargetDoorState'
|
||||
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
|
||||
CHAR_TARGET_POSITION = 'TargetPosition'
|
||||
CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100]
|
||||
CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
|
||||
CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
|
||||
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
|
||||
|
||||
# #### Properties ####
|
||||
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}
|
||||
|
||||
# #### Device Class ####
|
||||
DEVICE_CLASS_CO2 = 'co2'
|
||||
DEVICE_CLASS_GAS = 'gas'
|
||||
DEVICE_CLASS_HUMIDITY = 'humidity'
|
||||
DEVICE_CLASS_LIGHT = 'light'
|
||||
DEVICE_CLASS_MOISTURE = 'moisture'
|
||||
DEVICE_CLASS_MOTION = 'motion'
|
||||
DEVICE_CLASS_OCCUPANCY = 'occupancy'
|
||||
DEVICE_CLASS_OPENING = 'opening'
|
||||
DEVICE_CLASS_PM25 = 'pm25'
|
||||
DEVICE_CLASS_SMOKE = 'smoke'
|
||||
DEVICE_CLASS_TEMPERATURE = 'temperature'
|
||||
|
|
|
@ -1,18 +1,67 @@
|
|||
"""Class to hold all cover accessories."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import ATTR_CURRENT_POSITION
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED,
|
||||
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_STOP_COVER,
|
||||
ATTR_SUPPORTED_FEATURES)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .accessories import HomeAccessory, add_preload_service, setup_char
|
||||
from .const import (
|
||||
CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING,
|
||||
CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
|
||||
|
||||
CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE,
|
||||
CATEGORY_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER,
|
||||
CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@TYPES.register('GarageDoorOpener')
|
||||
class GarageDoorOpener(HomeAccessory):
|
||||
"""Generate a Garage Door Opener accessory for a cover entity.
|
||||
|
||||
The cover entity must be in the 'garage' device class
|
||||
and support no more than open, close, and stop.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
"""Initialize a GarageDoorOpener accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER)
|
||||
self.flag_target_state = False
|
||||
|
||||
serv_garage_door = add_preload_service(self, SERV_GARAGE_DOOR_OPENER)
|
||||
self.char_current_state = setup_char(
|
||||
CHAR_CURRENT_DOOR_STATE, serv_garage_door, value=0)
|
||||
self.char_target_state = setup_char(
|
||||
CHAR_TARGET_DOOR_STATE, serv_garage_door, value=0,
|
||||
callback=self.set_state)
|
||||
|
||||
def set_state(self, value):
|
||||
"""Change garage state if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
|
||||
self.flag_target_state = True
|
||||
|
||||
if value == 0:
|
||||
self.char_current_state.set_value(3)
|
||||
self.hass.components.cover.open_cover(self.entity_id)
|
||||
elif value == 1:
|
||||
self.char_current_state.set_value(2)
|
||||
self.hass.components.cover.close_cover(self.entity_id)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update cover state after state changed."""
|
||||
hass_state = new_state.state
|
||||
if hass_state in (STATE_OPEN, STATE_CLOSED):
|
||||
current_state = 0 if hass_state == STATE_OPEN else 1
|
||||
self.char_current_state.set_value(current_state)
|
||||
if not self.flag_target_state:
|
||||
self.char_target_state.set_value(current_state)
|
||||
self.flag_target_state = False
|
||||
|
||||
|
||||
@TYPES.register('WindowCovering')
|
||||
class WindowCovering(HomeAccessory):
|
||||
"""Generate a Window accessory for a cover entity.
|
||||
|
@ -20,54 +69,91 @@ class WindowCovering(HomeAccessory):
|
|||
The cover entity must support: set_cover_position.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name, **kwargs):
|
||||
def __init__(self, *args, config):
|
||||
"""Initialize a WindowCovering accessory object."""
|
||||
super().__init__(display_name, entity_id,
|
||||
CATEGORY_WINDOW_COVERING, **kwargs)
|
||||
|
||||
self.hass = hass
|
||||
self.entity_id = entity_id
|
||||
|
||||
self.current_position = None
|
||||
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
|
||||
self.homekit_target = None
|
||||
|
||||
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
|
||||
self.char_current_position = serv_cover. \
|
||||
get_characteristic(CHAR_CURRENT_POSITION)
|
||||
self.char_target_position = serv_cover. \
|
||||
get_characteristic(CHAR_TARGET_POSITION)
|
||||
self.char_position_state = serv_cover. \
|
||||
get_characteristic(CHAR_POSITION_STATE)
|
||||
self.char_current_position.value = 0
|
||||
self.char_target_position.value = 0
|
||||
self.char_position_state.value = 0
|
||||
|
||||
self.char_target_position.setter_callback = self.move_cover
|
||||
self.char_current_position = setup_char(
|
||||
CHAR_CURRENT_POSITION, serv_cover, value=0)
|
||||
self.char_target_position = setup_char(
|
||||
CHAR_TARGET_POSITION, serv_cover, value=0,
|
||||
callback=self.move_cover)
|
||||
|
||||
def move_cover(self, value):
|
||||
"""Move cover to value if call came from HomeKit."""
|
||||
self.char_target_position.set_value(value, should_callback=False)
|
||||
if value != self.current_position:
|
||||
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
|
||||
self.homekit_target = value
|
||||
if value > self.current_position:
|
||||
self.char_position_state.set_value(1)
|
||||
elif value < self.current_position:
|
||||
self.char_position_state.set_value(0)
|
||||
self.hass.components.cover.set_cover_position(
|
||||
value, self.entity_id)
|
||||
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
|
||||
self.homekit_target = value
|
||||
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_POSITION: value}
|
||||
self.hass.services.call(DOMAIN, SERVICE_SET_COVER_POSITION, params)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update cover position after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
if isinstance(current_position, int):
|
||||
self.current_position = current_position
|
||||
self.char_current_position.set_value(self.current_position)
|
||||
self.char_current_position.set_value(current_position)
|
||||
if self.homekit_target is None or \
|
||||
abs(self.current_position - self.homekit_target) < 6:
|
||||
self.char_target_position.set_value(self.current_position)
|
||||
self.char_position_state.set_value(2)
|
||||
abs(current_position - self.homekit_target) < 6:
|
||||
self.char_target_position.set_value(current_position)
|
||||
self.homekit_target = None
|
||||
|
||||
|
||||
@TYPES.register('WindowCoveringBasic')
|
||||
class WindowCoveringBasic(HomeAccessory):
|
||||
"""Generate a Window accessory for a cover entity.
|
||||
|
||||
The cover entity must support: open_cover, close_cover,
|
||||
stop_cover (optional).
|
||||
"""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
"""Initialize a WindowCovering accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
|
||||
features = self.hass.states.get(self.entity_id) \
|
||||
.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
self.supports_stop = features & SUPPORT_STOP
|
||||
|
||||
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
|
||||
self.char_current_position = setup_char(
|
||||
CHAR_CURRENT_POSITION, serv_cover, value=0)
|
||||
self.char_target_position = setup_char(
|
||||
CHAR_TARGET_POSITION, serv_cover, value=0,
|
||||
callback=self.move_cover)
|
||||
self.char_position_state = setup_char(
|
||||
CHAR_POSITION_STATE, serv_cover, value=2)
|
||||
|
||||
def move_cover(self, value):
|
||||
"""Move cover to value if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
|
||||
|
||||
if self.supports_stop:
|
||||
if value > 70:
|
||||
service, position = (SERVICE_OPEN_COVER, 100)
|
||||
elif value < 30:
|
||||
service, position = (SERVICE_CLOSE_COVER, 0)
|
||||
else:
|
||||
service, position = (SERVICE_STOP_COVER, 50)
|
||||
else:
|
||||
if value >= 50:
|
||||
service, position = (SERVICE_OPEN_COVER, 100)
|
||||
else:
|
||||
service, position = (SERVICE_CLOSE_COVER, 0)
|
||||
|
||||
self.hass.services.call(DOMAIN, service,
|
||||
{ATTR_ENTITY_ID: self.entity_id})
|
||||
|
||||
# Snap the current/target position to the expected final position.
|
||||
self.char_current_position.set_value(position)
|
||||
self.char_target_position.set_value(position)
|
||||
self.char_position_state.set_value(2)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update cover position after state changed."""
|
||||
position_mapping = {STATE_OPEN: 100, STATE_CLOSED: 0}
|
||||
hk_position = position_mapping.get(new_state.state)
|
||||
if hk_position is not None:
|
||||
self.char_current_position.set_value(hk_position)
|
||||
self.char_target_position.set_value(hk_position)
|
||||
self.char_position_state.set_value(2)
|
||||
|
|
|
@ -7,7 +7,8 @@ from homeassistant.components.light import (
|
|||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .accessories import (
|
||||
HomeAccessory, add_preload_service, debounce, setup_char)
|
||||
from .const import (
|
||||
CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
|
||||
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
|
||||
|
@ -24,12 +25,9 @@ class Light(HomeAccessory):
|
|||
Currently supports: state, brightness, color temperature, rgb_color.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity_id, name, **kwargs):
|
||||
def __init__(self, *args, config):
|
||||
"""Initialize a new Light accessory object."""
|
||||
super().__init__(name, entity_id, CATEGORY_LIGHT, **kwargs)
|
||||
|
||||
self.hass = hass
|
||||
self.entity_id = entity_id
|
||||
super().__init__(*args, category=CATEGORY_LIGHT)
|
||||
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
|
||||
CHAR_HUE: False, CHAR_SATURATION: False,
|
||||
CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False}
|
||||
|
@ -49,36 +47,29 @@ class Light(HomeAccessory):
|
|||
self._saturation = None
|
||||
|
||||
serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars)
|
||||
self.char_on = serv_light.get_characteristic(CHAR_ON)
|
||||
self.char_on.setter_callback = self.set_state
|
||||
self.char_on.value = self._state
|
||||
self.char_on = setup_char(
|
||||
CHAR_ON, serv_light, value=self._state, callback=self.set_state)
|
||||
|
||||
if CHAR_BRIGHTNESS in self.chars:
|
||||
self.char_brightness = serv_light \
|
||||
.get_characteristic(CHAR_BRIGHTNESS)
|
||||
self.char_brightness.setter_callback = self.set_brightness
|
||||
self.char_brightness.value = 0
|
||||
self.char_brightness = setup_char(
|
||||
CHAR_BRIGHTNESS, serv_light, value=0,
|
||||
callback=self.set_brightness)
|
||||
if CHAR_COLOR_TEMPERATURE in self.chars:
|
||||
self.char_color_temperature = serv_light \
|
||||
.get_characteristic(CHAR_COLOR_TEMPERATURE)
|
||||
self.char_color_temperature.setter_callback = \
|
||||
self.set_color_temperature
|
||||
min_mireds = self.hass.states.get(self.entity_id) \
|
||||
.attributes.get(ATTR_MIN_MIREDS, 153)
|
||||
max_mireds = self.hass.states.get(self.entity_id) \
|
||||
.attributes.get(ATTR_MAX_MIREDS, 500)
|
||||
self.char_color_temperature.override_properties({
|
||||
'minValue': min_mireds, 'maxValue': max_mireds})
|
||||
self.char_color_temperature.value = min_mireds
|
||||
self.char_color_temperature = setup_char(
|
||||
CHAR_COLOR_TEMPERATURE, serv_light, value=min_mireds,
|
||||
properties={'minValue': min_mireds, 'maxValue': max_mireds},
|
||||
callback=self.set_color_temperature)
|
||||
if CHAR_HUE in self.chars:
|
||||
self.char_hue = serv_light.get_characteristic(CHAR_HUE)
|
||||
self.char_hue.setter_callback = self.set_hue
|
||||
self.char_hue.value = 0
|
||||
self.char_hue = setup_char(
|
||||
CHAR_HUE, serv_light, value=0, callback=self.set_hue)
|
||||
if CHAR_SATURATION in self.chars:
|
||||
self.char_saturation = serv_light \
|
||||
.get_characteristic(CHAR_SATURATION)
|
||||
self.char_saturation.setter_callback = self.set_saturation
|
||||
self.char_saturation.value = 75
|
||||
self.char_saturation = setup_char(
|
||||
CHAR_SATURATION, serv_light, value=75,
|
||||
callback=self.set_saturation)
|
||||
|
||||
def set_state(self, value):
|
||||
"""Set state if call came from HomeKit."""
|
||||
|
@ -87,18 +78,17 @@ class Light(HomeAccessory):
|
|||
|
||||
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
|
||||
self._flag[CHAR_ON] = True
|
||||
self.char_on.set_value(value, should_callback=False)
|
||||
|
||||
if value == 1:
|
||||
self.hass.components.light.turn_on(self.entity_id)
|
||||
elif value == 0:
|
||||
self.hass.components.light.turn_off(self.entity_id)
|
||||
|
||||
@debounce
|
||||
def set_brightness(self, value):
|
||||
"""Set brightness if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set brightness to %d', self.entity_id, value)
|
||||
self._flag[CHAR_BRIGHTNESS] = True
|
||||
self.char_brightness.set_value(value, should_callback=False)
|
||||
if value != 0:
|
||||
self.hass.components.light.turn_on(
|
||||
self.entity_id, brightness_pct=value)
|
||||
|
@ -109,14 +99,12 @@ class Light(HomeAccessory):
|
|||
"""Set color temperature if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set color temp to %s', self.entity_id, value)
|
||||
self._flag[CHAR_COLOR_TEMPERATURE] = True
|
||||
self.char_color_temperature.set_value(value, should_callback=False)
|
||||
self.hass.components.light.turn_on(self.entity_id, color_temp=value)
|
||||
|
||||
def set_saturation(self, value):
|
||||
"""Set saturation if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set saturation to %d', self.entity_id, value)
|
||||
self._flag[CHAR_SATURATION] = True
|
||||
self.char_saturation.set_value(value, should_callback=False)
|
||||
self._saturation = value
|
||||
self.set_color()
|
||||
|
||||
|
@ -124,7 +112,6 @@ class Light(HomeAccessory):
|
|||
"""Set hue if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set hue to %d', self.entity_id, value)
|
||||
self._flag[CHAR_HUE] = True
|
||||
self.char_hue.set_value(value, should_callback=False)
|
||||
self._hue = value
|
||||
self.set_color()
|
||||
|
||||
|
@ -140,17 +127,14 @@ class Light(HomeAccessory):
|
|||
self.hass.components.light.turn_on(
|
||||
self.entity_id, hs_color=color)
|
||||
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
def update_state(self, new_state):
|
||||
"""Update light after state change."""
|
||||
if not new_state:
|
||||
return
|
||||
|
||||
# Handle State
|
||||
state = new_state.state
|
||||
if state in (STATE_ON, STATE_OFF):
|
||||
self._state = 1 if state == STATE_ON else 0
|
||||
if not self._flag[CHAR_ON] and self.char_on.value != self._state:
|
||||
self.char_on.set_value(self._state, should_callback=False)
|
||||
self.char_on.set_value(self._state)
|
||||
self._flag[CHAR_ON] = False
|
||||
|
||||
# Handle Brightness
|
||||
|
@ -159,17 +143,16 @@ class Light(HomeAccessory):
|
|||
if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int):
|
||||
brightness = round(brightness / 255 * 100, 0)
|
||||
if self.char_brightness.value != brightness:
|
||||
self.char_brightness.set_value(brightness,
|
||||
should_callback=False)
|
||||
self.char_brightness.set_value(brightness)
|
||||
self._flag[CHAR_BRIGHTNESS] = False
|
||||
|
||||
# Handle color temperature
|
||||
if CHAR_COLOR_TEMPERATURE in self.chars:
|
||||
color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP)
|
||||
if not self._flag[CHAR_COLOR_TEMPERATURE] \
|
||||
and isinstance(color_temperature, int):
|
||||
self.char_color_temperature.set_value(color_temperature,
|
||||
should_callback=False)
|
||||
and isinstance(color_temperature, int) and \
|
||||
self.char_color_temperature.value != color_temperature:
|
||||
self.char_color_temperature.set_value(color_temperature)
|
||||
self._flag[CHAR_COLOR_TEMPERATURE] = False
|
||||
|
||||
# Handle Color
|
||||
|
@ -180,8 +163,7 @@ class Light(HomeAccessory):
|
|||
hue != self._hue or saturation != self._saturation) and \
|
||||
isinstance(hue, (int, float)) and \
|
||||
isinstance(saturation, (int, float)):
|
||||
self.char_hue.set_value(hue, should_callback=False)
|
||||
self.char_saturation.set_value(saturation,
|
||||
should_callback=False)
|
||||
self.char_hue.set_value(hue)
|
||||
self.char_saturation.set_value(saturation)
|
||||
self._hue, self._saturation = (hue, saturation)
|
||||
self._flag[RGB_COLOR] = False
|
||||
|
|
67
homeassistant/components/homekit/type_locks.py
Normal file
67
homeassistant/components/homekit/type_locks.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
"""Class to hold all lock accessories."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.lock import (
|
||||
ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service, setup_char
|
||||
from .const import (
|
||||
CATEGORY_LOCK, SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HASS_TO_HOMEKIT = {STATE_UNLOCKED: 0,
|
||||
STATE_LOCKED: 1,
|
||||
# value 2 is Jammed which hass doesn't have a state for
|
||||
STATE_UNKNOWN: 3}
|
||||
HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()}
|
||||
STATE_TO_SERVICE = {STATE_LOCKED: 'lock',
|
||||
STATE_UNLOCKED: 'unlock'}
|
||||
|
||||
|
||||
@TYPES.register('Lock')
|
||||
class Lock(HomeAccessory):
|
||||
"""Generate a Lock accessory for a lock entity.
|
||||
|
||||
The lock entity must support: unlock and lock.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
"""Initialize a Lock accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_LOCK)
|
||||
self.flag_target_state = False
|
||||
|
||||
serv_lock_mechanism = add_preload_service(self, SERV_LOCK)
|
||||
self.char_current_state = setup_char(
|
||||
CHAR_LOCK_CURRENT_STATE, serv_lock_mechanism,
|
||||
value=HASS_TO_HOMEKIT[STATE_UNKNOWN])
|
||||
self.char_target_state = setup_char(
|
||||
CHAR_LOCK_TARGET_STATE, serv_lock_mechanism,
|
||||
value=HASS_TO_HOMEKIT[STATE_LOCKED], callback=self.set_state)
|
||||
|
||||
def set_state(self, value):
|
||||
"""Set lock state to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set state to %d", self.entity_id, value)
|
||||
self.flag_target_state = True
|
||||
|
||||
hass_value = HOMEKIT_TO_HASS.get(value)
|
||||
service = STATE_TO_SERVICE[hass_value]
|
||||
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
self.hass.services.call('lock', service, params)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update lock after state changed."""
|
||||
hass_state = new_state.state
|
||||
if hass_state in HASS_TO_HOMEKIT:
|
||||
current_lock_state = HASS_TO_HOMEKIT[hass_state]
|
||||
self.char_current_state.set_value(current_lock_state)
|
||||
_LOGGER.debug('%s: Updated current state to %s (%d)',
|
||||
self.entity_id, hass_state, current_lock_state)
|
||||
|
||||
# LockTargetState only supports locked and unlocked
|
||||
if hass_state in (STATE_LOCKED, STATE_UNLOCKED):
|
||||
if not self.flag_target_state:
|
||||
self.char_target_state.set_value(current_lock_state)
|
||||
self.flag_target_state = False
|
|
@ -7,7 +7,7 @@ from homeassistant.const import (
|
|||
ATTR_ENTITY_ID, ATTR_CODE)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .accessories import HomeAccessory, add_preload_service, setup_char
|
||||
from .const import (
|
||||
CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM,
|
||||
CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE)
|
||||
|
@ -27,33 +27,24 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
|
|||
class SecuritySystem(HomeAccessory):
|
||||
"""Generate an SecuritySystem accessory for an alarm control panel."""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name, alarm_code, **kwargs):
|
||||
def __init__(self, *args, config):
|
||||
"""Initialize a SecuritySystem accessory object."""
|
||||
super().__init__(display_name, entity_id,
|
||||
CATEGORY_ALARM_SYSTEM, **kwargs)
|
||||
|
||||
self.hass = hass
|
||||
self.entity_id = entity_id
|
||||
self._alarm_code = alarm_code
|
||||
|
||||
super().__init__(*args, category=CATEGORY_ALARM_SYSTEM)
|
||||
self._alarm_code = config[ATTR_CODE]
|
||||
self.flag_target_state = False
|
||||
|
||||
serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
|
||||
self.char_current_state = serv_alarm. \
|
||||
get_characteristic(CHAR_CURRENT_SECURITY_STATE)
|
||||
self.char_current_state.value = 3
|
||||
self.char_target_state = serv_alarm. \
|
||||
get_characteristic(CHAR_TARGET_SECURITY_STATE)
|
||||
self.char_target_state.value = 3
|
||||
|
||||
self.char_target_state.setter_callback = self.set_security_state
|
||||
self.char_current_state = setup_char(
|
||||
CHAR_CURRENT_SECURITY_STATE, serv_alarm, value=3)
|
||||
self.char_target_state = setup_char(
|
||||
CHAR_TARGET_SECURITY_STATE, serv_alarm, value=3,
|
||||
callback=self.set_security_state)
|
||||
|
||||
def set_security_state(self, value):
|
||||
"""Move security state to value if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set security state to %d',
|
||||
self.entity_id, value)
|
||||
self.flag_target_state = True
|
||||
self.char_target_state.set_value(value, should_callback=False)
|
||||
hass_value = HOMEKIT_TO_HASS[value]
|
||||
service = STATE_TO_SERVICE[hass_value]
|
||||
|
||||
|
@ -62,23 +53,16 @@ class SecuritySystem(HomeAccessory):
|
|||
params[ATTR_CODE] = self._alarm_code
|
||||
self.hass.services.call('alarm_control_panel', service, params)
|
||||
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
def update_state(self, new_state):
|
||||
"""Update security state after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
hass_state = new_state.state
|
||||
if hass_state not in HASS_TO_HOMEKIT:
|
||||
return
|
||||
if hass_state in HASS_TO_HOMEKIT:
|
||||
current_security_state = HASS_TO_HOMEKIT[hass_state]
|
||||
self.char_current_state.set_value(current_security_state)
|
||||
_LOGGER.debug('%s: Updated current state to %s (%d)',
|
||||
self.entity_id, hass_state, current_security_state)
|
||||
|
||||
current_security_state = HASS_TO_HOMEKIT[hass_state]
|
||||
self.char_current_state.set_value(current_security_state,
|
||||
should_callback=False)
|
||||
_LOGGER.debug('%s: Updated current state to %s (%d)',
|
||||
self.entity_id, hass_state, current_security_state)
|
||||
|
||||
if not self.flag_target_state:
|
||||
self.char_target_state.set_value(current_security_state,
|
||||
should_callback=False)
|
||||
if self.char_target_state.value == self.char_current_state.value:
|
||||
self.flag_target_state = False
|
||||
if not self.flag_target_state:
|
||||
self.char_target_state.set_value(current_security_state)
|
||||
if self.char_target_state.value == self.char_current_state.value:
|
||||
self.flag_target_state = False
|
||||
|
|
|
@ -2,18 +2,41 @@
|
|||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
|
||||
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS,
|
||||
ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .accessories import HomeAccessory, add_preload_service, setup_char
|
||||
from .const import (
|
||||
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
|
||||
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
|
||||
from .util import convert_to_float, temperature_to_homekit
|
||||
|
||||
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS,
|
||||
SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY,
|
||||
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL,
|
||||
SERV_LIGHT_SENSOR, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL,
|
||||
DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED,
|
||||
DEVICE_CLASS_GAS, SERV_CARBON_MONOXIDE_SENSOR,
|
||||
CHAR_CARBON_MONOXIDE_DETECTED,
|
||||
DEVICE_CLASS_MOISTURE, SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED,
|
||||
DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED,
|
||||
DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED,
|
||||
DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE,
|
||||
DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED)
|
||||
from .util import (
|
||||
convert_to_float, temperature_to_homekit, density_to_air_quality)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BINARY_SENSOR_SERVICE_MAP = {
|
||||
DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR,
|
||||
CHAR_CARBON_DIOXIDE_DETECTED),
|
||||
DEVICE_CLASS_GAS: (SERV_CARBON_MONOXIDE_SENSOR,
|
||||
CHAR_CARBON_MONOXIDE_DETECTED),
|
||||
DEVICE_CLASS_MOISTURE: (SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED),
|
||||
DEVICE_CLASS_MOTION: (SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED),
|
||||
DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED),
|
||||
DEVICE_CLASS_OPENING: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE),
|
||||
DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED)}
|
||||
|
||||
|
||||
@TYPES.register('TemperatureSensor')
|
||||
class TemperatureSensor(HomeAccessory):
|
||||
|
@ -22,29 +45,22 @@ class TemperatureSensor(HomeAccessory):
|
|||
Sensor entity must return temperature in °C, °F.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity_id, name, **kwargs):
|
||||
def __init__(self, *args, config):
|
||||
"""Initialize a TemperatureSensor accessory object."""
|
||||
super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs)
|
||||
|
||||
self.hass = hass
|
||||
self.entity_id = entity_id
|
||||
|
||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
|
||||
self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE)
|
||||
self.char_temp.override_properties(properties=PROP_CELSIUS)
|
||||
self.char_temp.value = 0
|
||||
self.char_temp = setup_char(
|
||||
CHAR_CURRENT_TEMPERATURE, serv_temp, value=0,
|
||||
properties=PROP_CELSIUS)
|
||||
self.unit = None
|
||||
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
def update_state(self, new_state):
|
||||
"""Update temperature after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
|
||||
temperature = convert_to_float(new_state.state)
|
||||
if temperature:
|
||||
temperature = temperature_to_homekit(temperature, unit)
|
||||
self.char_temp.set_value(temperature, should_callback=False)
|
||||
self.char_temp.set_value(temperature)
|
||||
_LOGGER.debug('%s: Current temperature set to %d°C',
|
||||
self.entity_id, temperature)
|
||||
|
||||
|
@ -53,25 +69,113 @@ class TemperatureSensor(HomeAccessory):
|
|||
class HumiditySensor(HomeAccessory):
|
||||
"""Generate a HumiditySensor accessory as humidity sensor."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, *args, **kwargs):
|
||||
def __init__(self, *args, config):
|
||||
"""Initialize a HumiditySensor accessory object."""
|
||||
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
|
||||
|
||||
self.hass = hass
|
||||
self.entity_id = entity_id
|
||||
|
||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
|
||||
self.char_humidity = serv_humidity \
|
||||
.get_characteristic(CHAR_CURRENT_HUMIDITY)
|
||||
self.char_humidity.value = 0
|
||||
self.char_humidity = setup_char(
|
||||
CHAR_CURRENT_HUMIDITY, serv_humidity, value=0)
|
||||
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
def update_state(self, new_state):
|
||||
"""Update accessory after state change."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
humidity = convert_to_float(new_state.state)
|
||||
if humidity:
|
||||
self.char_humidity.set_value(humidity, should_callback=False)
|
||||
self.char_humidity.set_value(humidity)
|
||||
_LOGGER.debug('%s: Percent set to %d%%',
|
||||
self.entity_id, humidity)
|
||||
|
||||
|
||||
@TYPES.register('AirQualitySensor')
|
||||
class AirQualitySensor(HomeAccessory):
|
||||
"""Generate a AirQualitySensor accessory as air quality sensor."""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
"""Initialize a AirQualitySensor accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||
|
||||
serv_air_quality = add_preload_service(self, SERV_AIR_QUALITY_SENSOR,
|
||||
[CHAR_AIR_PARTICULATE_DENSITY])
|
||||
self.char_quality = setup_char(
|
||||
CHAR_AIR_QUALITY, serv_air_quality, value=0)
|
||||
self.char_density = setup_char(
|
||||
CHAR_AIR_PARTICULATE_DENSITY, serv_air_quality, value=0)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update accessory after state change."""
|
||||
density = convert_to_float(new_state.state)
|
||||
if density is not None:
|
||||
self.char_density.set_value(density)
|
||||
self.char_quality.set_value(density_to_air_quality(density))
|
||||
_LOGGER.debug('%s: Set to %d', self.entity_id, density)
|
||||
|
||||
|
||||
@TYPES.register('CarbonDioxideSensor')
|
||||
class CarbonDioxideSensor(HomeAccessory):
|
||||
"""Generate a CarbonDioxideSensor accessory as CO2 sensor."""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
"""Initialize a CarbonDioxideSensor accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||
|
||||
serv_co2 = add_preload_service(self, SERV_CARBON_DIOXIDE_SENSOR, [
|
||||
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL])
|
||||
self.char_co2 = setup_char(
|
||||
CHAR_CARBON_DIOXIDE_LEVEL, serv_co2, value=0)
|
||||
self.char_peak = setup_char(
|
||||
CHAR_CARBON_DIOXIDE_PEAK_LEVEL, serv_co2, value=0)
|
||||
self.char_detected = setup_char(
|
||||
CHAR_CARBON_DIOXIDE_DETECTED, serv_co2, value=0)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update accessory after state change."""
|
||||
co2 = convert_to_float(new_state.state)
|
||||
if co2 is not None:
|
||||
self.char_co2.set_value(co2)
|
||||
if co2 > self.char_peak.value:
|
||||
self.char_peak.set_value(co2)
|
||||
self.char_detected.set_value(co2 > 1000)
|
||||
_LOGGER.debug('%s: Set to %d', self.entity_id, co2)
|
||||
|
||||
|
||||
@TYPES.register('LightSensor')
|
||||
class LightSensor(HomeAccessory):
|
||||
"""Generate a LightSensor accessory as light sensor."""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
"""Initialize a LightSensor accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||
|
||||
serv_light = add_preload_service(self, SERV_LIGHT_SENSOR)
|
||||
self.char_light = setup_char(
|
||||
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, serv_light, value=0)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update accessory after state change."""
|
||||
luminance = convert_to_float(new_state.state)
|
||||
if luminance is not None:
|
||||
self.char_light.set_value(luminance)
|
||||
_LOGGER.debug('%s: Set to %d', self.entity_id, luminance)
|
||||
|
||||
|
||||
@TYPES.register('BinarySensor')
|
||||
class BinarySensor(HomeAccessory):
|
||||
"""Generate a BinarySensor accessory as binary sensor."""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
"""Initialize a BinarySensor accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||
device_class = self.hass.states.get(self.entity_id).attributes \
|
||||
.get(ATTR_DEVICE_CLASS)
|
||||
service_char = BINARY_SENSOR_SERVICE_MAP[device_class] \
|
||||
if device_class in BINARY_SENSOR_SERVICE_MAP \
|
||||
else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY]
|
||||
|
||||
service = add_preload_service(self, service_char[0])
|
||||
self.char_detected = setup_char(service_char[1], service, value=0)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update accessory after state change."""
|
||||
state = new_state.state
|
||||
detected = (state == STATE_ON) or (state == STATE_HOME)
|
||||
self.char_detected.set_value(detected)
|
||||
_LOGGER.debug('%s: Set to %d', self.entity_id, detected)
|
||||
|
|
|
@ -6,7 +6,7 @@ from homeassistant.const import (
|
|||
from homeassistant.core import split_entity_id
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .accessories import HomeAccessory, add_preload_service, setup_char
|
||||
from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -16,40 +16,30 @@ _LOGGER = logging.getLogger(__name__)
|
|||
class Switch(HomeAccessory):
|
||||
"""Generate a Switch accessory."""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name, **kwargs):
|
||||
def __init__(self, *args, config):
|
||||
"""Initialize a Switch accessory object to represent a remote."""
|
||||
super().__init__(display_name, entity_id, CATEGORY_SWITCH, **kwargs)
|
||||
|
||||
self.hass = hass
|
||||
self.entity_id = entity_id
|
||||
self._domain = split_entity_id(entity_id)[0]
|
||||
|
||||
super().__init__(*args, category=CATEGORY_SWITCH)
|
||||
self._domain = split_entity_id(self.entity_id)[0]
|
||||
self.flag_target_state = False
|
||||
|
||||
serv_switch = add_preload_service(self, SERV_SWITCH)
|
||||
self.char_on = serv_switch.get_characteristic(CHAR_ON)
|
||||
self.char_on.value = False
|
||||
self.char_on.setter_callback = self.set_state
|
||||
self.char_on = setup_char(
|
||||
CHAR_ON, serv_switch, value=False, callback=self.set_state)
|
||||
|
||||
def set_state(self, value):
|
||||
"""Move switch state to value if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set switch state to %s',
|
||||
self.entity_id, value)
|
||||
self.flag_target_state = True
|
||||
self.char_on.set_value(value, should_callback=False)
|
||||
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
|
||||
self.hass.services.call(self._domain, service,
|
||||
{ATTR_ENTITY_ID: self.entity_id})
|
||||
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
def update_state(self, new_state):
|
||||
"""Update switch state after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
current_state = (new_state.state == STATE_ON)
|
||||
if not self.flag_target_state:
|
||||
_LOGGER.debug('%s: Set current state to %s',
|
||||
self.entity_id, current_state)
|
||||
self.char_on.set_value(current_state, should_callback=False)
|
||||
|
||||
self.char_on.set_value(current_state)
|
||||
self.flag_target_state = False
|
||||
|
|
|
@ -5,12 +5,15 @@ from homeassistant.components.climate import (
|
|||
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE,
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
|
||||
STATE_HEAT, STATE_COOL, STATE_AUTO)
|
||||
STATE_HEAT, STATE_COOL, STATE_AUTO,
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
||||
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .accessories import (
|
||||
HomeAccessory, add_preload_service, debounce, setup_char)
|
||||
from .const import (
|
||||
CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
|
||||
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
|
||||
|
@ -26,78 +29,66 @@ HC_HASS_TO_HOMEKIT = {STATE_OFF: 0, STATE_HEAT: 1,
|
|||
STATE_COOL: 2, STATE_AUTO: 3}
|
||||
HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()}
|
||||
|
||||
SUPPORT_TEMP_RANGE = SUPPORT_TARGET_TEMPERATURE_LOW | \
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH
|
||||
|
||||
|
||||
@TYPES.register('Thermostat')
|
||||
class Thermostat(HomeAccessory):
|
||||
"""Generate a Thermostat accessory for a climate."""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name, support_auto, **kwargs):
|
||||
def __init__(self, *args, config):
|
||||
"""Initialize a Thermostat accessory object."""
|
||||
super().__init__(display_name, entity_id,
|
||||
CATEGORY_THERMOSTAT, **kwargs)
|
||||
|
||||
self.hass = hass
|
||||
self.entity_id = entity_id
|
||||
self._call_timer = None
|
||||
super().__init__(*args, category=CATEGORY_THERMOSTAT)
|
||||
self._unit = TEMP_CELSIUS
|
||||
|
||||
self.heat_cool_flag_target_state = False
|
||||
self.temperature_flag_target_state = False
|
||||
self.coolingthresh_flag_target_state = False
|
||||
self.heatingthresh_flag_target_state = False
|
||||
|
||||
# Add additional characteristics if auto mode is supported
|
||||
extra_chars = [
|
||||
CHAR_COOLING_THRESHOLD_TEMPERATURE,
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE] if support_auto else None
|
||||
self.chars = []
|
||||
features = self.hass.states.get(self.entity_id) \
|
||||
.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
if features & SUPPORT_TEMP_RANGE:
|
||||
self.chars.extend((CHAR_COOLING_THRESHOLD_TEMPERATURE,
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE))
|
||||
|
||||
# Preload the thermostat service
|
||||
serv_thermostat = add_preload_service(self, SERV_THERMOSTAT,
|
||||
extra_chars)
|
||||
serv_thermostat = add_preload_service(
|
||||
self, SERV_THERMOSTAT, self.chars)
|
||||
|
||||
# Current and target mode characteristics
|
||||
self.char_current_heat_cool = serv_thermostat. \
|
||||
get_characteristic(CHAR_CURRENT_HEATING_COOLING)
|
||||
self.char_current_heat_cool.value = 0
|
||||
self.char_target_heat_cool = serv_thermostat. \
|
||||
get_characteristic(CHAR_TARGET_HEATING_COOLING)
|
||||
self.char_target_heat_cool.value = 0
|
||||
self.char_target_heat_cool.setter_callback = self.set_heat_cool
|
||||
self.char_current_heat_cool = setup_char(
|
||||
CHAR_CURRENT_HEATING_COOLING, serv_thermostat, value=0)
|
||||
self.char_target_heat_cool = setup_char(
|
||||
CHAR_TARGET_HEATING_COOLING, serv_thermostat, value=0,
|
||||
callback=self.set_heat_cool)
|
||||
|
||||
# Current and target temperature characteristics
|
||||
self.char_current_temp = serv_thermostat. \
|
||||
get_characteristic(CHAR_CURRENT_TEMPERATURE)
|
||||
self.char_current_temp.value = 21.0
|
||||
self.char_target_temp = serv_thermostat. \
|
||||
get_characteristic(CHAR_TARGET_TEMPERATURE)
|
||||
self.char_target_temp.value = 21.0
|
||||
self.char_target_temp.setter_callback = self.set_target_temperature
|
||||
self.char_current_temp = setup_char(
|
||||
CHAR_CURRENT_TEMPERATURE, serv_thermostat, value=21.0)
|
||||
self.char_target_temp = setup_char(
|
||||
CHAR_TARGET_TEMPERATURE, serv_thermostat, value=21.0,
|
||||
callback=self.set_target_temperature)
|
||||
|
||||
# Display units characteristic
|
||||
self.char_display_units = serv_thermostat. \
|
||||
get_characteristic(CHAR_TEMP_DISPLAY_UNITS)
|
||||
self.char_display_units.value = 0
|
||||
self.char_display_units = setup_char(
|
||||
CHAR_TEMP_DISPLAY_UNITS, serv_thermostat, value=0)
|
||||
|
||||
# If the device supports it: high and low temperature characteristics
|
||||
if support_auto:
|
||||
self.char_cooling_thresh_temp = serv_thermostat. \
|
||||
get_characteristic(CHAR_COOLING_THRESHOLD_TEMPERATURE)
|
||||
self.char_cooling_thresh_temp.value = 23.0
|
||||
self.char_cooling_thresh_temp.setter_callback = \
|
||||
self.set_cooling_threshold
|
||||
|
||||
self.char_heating_thresh_temp = serv_thermostat. \
|
||||
get_characteristic(CHAR_HEATING_THRESHOLD_TEMPERATURE)
|
||||
self.char_heating_thresh_temp.value = 19.0
|
||||
self.char_heating_thresh_temp.setter_callback = \
|
||||
self.set_heating_threshold
|
||||
else:
|
||||
self.char_cooling_thresh_temp = None
|
||||
self.char_heating_thresh_temp = None
|
||||
self.char_cooling_thresh_temp = None
|
||||
self.char_heating_thresh_temp = None
|
||||
if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars:
|
||||
self.char_cooling_thresh_temp = setup_char(
|
||||
CHAR_COOLING_THRESHOLD_TEMPERATURE, serv_thermostat,
|
||||
value=23.0, callback=self.set_cooling_threshold)
|
||||
if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars:
|
||||
self.char_heating_thresh_temp = setup_char(
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE, serv_thermostat,
|
||||
value=19.0, callback=self.set_heating_threshold)
|
||||
|
||||
def set_heat_cool(self, value):
|
||||
"""Move operation mode to value if call came from HomeKit."""
|
||||
self.char_target_heat_cool.set_value(value, should_callback=False)
|
||||
if value in HC_HOMEKIT_TO_HASS:
|
||||
_LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
|
||||
self.heat_cool_flag_target_state = True
|
||||
|
@ -105,12 +96,12 @@ class Thermostat(HomeAccessory):
|
|||
self.hass.components.climate.set_operation_mode(
|
||||
operation_mode=hass_value, entity_id=self.entity_id)
|
||||
|
||||
@debounce
|
||||
def set_cooling_threshold(self, value):
|
||||
"""Set cooling threshold temp to value if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C',
|
||||
self.entity_id, value)
|
||||
self.coolingthresh_flag_target_state = True
|
||||
self.char_cooling_thresh_temp.set_value(value, should_callback=False)
|
||||
low = self.char_heating_thresh_temp.value
|
||||
low = temperature_to_states(low, self._unit)
|
||||
value = temperature_to_states(value, self._unit)
|
||||
|
@ -118,12 +109,12 @@ class Thermostat(HomeAccessory):
|
|||
entity_id=self.entity_id, target_temp_high=value,
|
||||
target_temp_low=low)
|
||||
|
||||
@debounce
|
||||
def set_heating_threshold(self, value):
|
||||
"""Set heating threshold temp to value if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
|
||||
self.entity_id, value)
|
||||
self.heatingthresh_flag_target_state = True
|
||||
self.char_heating_thresh_temp.set_value(value, should_callback=False)
|
||||
# Home assistant always wants to set low and high at the same time
|
||||
high = self.char_cooling_thresh_temp.value
|
||||
high = temperature_to_states(high, self._unit)
|
||||
|
@ -132,21 +123,18 @@ class Thermostat(HomeAccessory):
|
|||
entity_id=self.entity_id, target_temp_high=high,
|
||||
target_temp_low=value)
|
||||
|
||||
@debounce
|
||||
def set_target_temperature(self, value):
|
||||
"""Set target temperature to value if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set target temperature to %.2f°C',
|
||||
self.entity_id, value)
|
||||
self.temperature_flag_target_state = True
|
||||
self.char_target_temp.set_value(value, should_callback=False)
|
||||
value = temperature_to_states(value, self._unit)
|
||||
self.hass.components.climate.set_temperature(
|
||||
temperature=value, entity_id=self.entity_id)
|
||||
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
def update_state(self, new_state):
|
||||
"""Update security state after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT,
|
||||
TEMP_CELSIUS)
|
||||
|
||||
|
@ -161,8 +149,7 @@ class Thermostat(HomeAccessory):
|
|||
if isinstance(target_temp, (int, float)):
|
||||
target_temp = temperature_to_homekit(target_temp, self._unit)
|
||||
if not self.temperature_flag_target_state:
|
||||
self.char_target_temp.set_value(target_temp,
|
||||
should_callback=False)
|
||||
self.char_target_temp.set_value(target_temp)
|
||||
self.temperature_flag_target_state = False
|
||||
|
||||
# Update cooling threshold temperature if characteristic exists
|
||||
|
@ -172,8 +159,7 @@ class Thermostat(HomeAccessory):
|
|||
cooling_thresh = temperature_to_homekit(cooling_thresh,
|
||||
self._unit)
|
||||
if not self.coolingthresh_flag_target_state:
|
||||
self.char_cooling_thresh_temp.set_value(
|
||||
cooling_thresh, should_callback=False)
|
||||
self.char_cooling_thresh_temp.set_value(cooling_thresh)
|
||||
self.coolingthresh_flag_target_state = False
|
||||
|
||||
# Update heating threshold temperature if characteristic exists
|
||||
|
@ -183,8 +169,7 @@ class Thermostat(HomeAccessory):
|
|||
heating_thresh = temperature_to_homekit(heating_thresh,
|
||||
self._unit)
|
||||
if not self.heatingthresh_flag_target_state:
|
||||
self.char_heating_thresh_temp.set_value(
|
||||
heating_thresh, should_callback=False)
|
||||
self.char_heating_thresh_temp.set_value(heating_thresh)
|
||||
self.heatingthresh_flag_target_state = False
|
||||
|
||||
# Update display units
|
||||
|
@ -197,7 +182,7 @@ class Thermostat(HomeAccessory):
|
|||
and operation_mode in HC_HASS_TO_HOMEKIT:
|
||||
if not self.heat_cool_flag_target_state:
|
||||
self.char_target_heat_cool.set_value(
|
||||
HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False)
|
||||
HC_HASS_TO_HOMEKIT[operation_mode])
|
||||
self.heat_cool_flag_target_state = False
|
||||
|
||||
# Set current operation mode based on temperatures and target mode
|
||||
|
|
|
@ -33,7 +33,7 @@ def validate_entity_config(values):
|
|||
return entities
|
||||
|
||||
|
||||
def show_setup_message(bridge, hass):
|
||||
def show_setup_message(hass, bridge):
|
||||
"""Display persistent notification with setup information."""
|
||||
pin = bridge.pincode.decode()
|
||||
_LOGGER.info('Pincode: %s', pin)
|
||||
|
@ -64,3 +64,16 @@ def temperature_to_homekit(temperature, unit):
|
|||
def temperature_to_states(temperature, unit):
|
||||
"""Convert temperature back from Celsius to Home Assistant unit."""
|
||||
return round(temp_util.convert(temperature, TEMP_CELSIUS, unit), 1)
|
||||
|
||||
|
||||
def density_to_air_quality(density):
|
||||
"""Map PM2.5 density to HomeKit AirQuality level."""
|
||||
if density <= 35:
|
||||
return 1
|
||||
elif density <= 75:
|
||||
return 2
|
||||
elif density <= 115:
|
||||
return 3
|
||||
elif density <= 150:
|
||||
return 4
|
||||
return 5
|
||||
|
|
228
homeassistant/components/homekit_controller/__init__.py
Normal file
228
homeassistant/components/homekit_controller/__init__.py
Normal file
|
@ -0,0 +1,228 @@
|
|||
"""
|
||||
Support for Homekit device discovery.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/homekit_controller/
|
||||
"""
|
||||
import http
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from homeassistant.components.discovery import SERVICE_HOMEKIT
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['homekit==0.5']
|
||||
|
||||
DOMAIN = 'homekit_controller'
|
||||
HOMEKIT_DIR = '.homekit'
|
||||
|
||||
# Mapping from Homekit type to component.
|
||||
HOMEKIT_ACCESSORY_DISPATCH = {
|
||||
'lightbulb': 'light',
|
||||
'outlet': 'switch',
|
||||
}
|
||||
|
||||
KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN)
|
||||
KNOWN_DEVICES = "{}-devices".format(DOMAIN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def homekit_http_send(self, message_body=None):
|
||||
r"""Send the currently buffered request and clear the buffer.
|
||||
|
||||
Appends an extra \r\n to the buffer.
|
||||
A message_body may be specified, to be appended to the request.
|
||||
"""
|
||||
self._buffer.extend((b"", b""))
|
||||
msg = b"\r\n".join(self._buffer)
|
||||
del self._buffer[:]
|
||||
|
||||
if message_body is not None:
|
||||
msg = msg + message_body
|
||||
|
||||
self.send(msg)
|
||||
|
||||
|
||||
def get_serial(accessory):
|
||||
"""Obtain the serial number of a HomeKit device."""
|
||||
# pylint: disable=import-error
|
||||
import homekit
|
||||
for service in accessory['services']:
|
||||
if homekit.ServicesTypes.get_short(service['type']) != \
|
||||
'accessory-information':
|
||||
continue
|
||||
for characteristic in service['characteristics']:
|
||||
ctype = homekit.CharacteristicsTypes.get_short(
|
||||
characteristic['type'])
|
||||
if ctype != 'serial-number':
|
||||
continue
|
||||
return characteristic['value']
|
||||
return None
|
||||
|
||||
|
||||
class HKDevice():
|
||||
"""HomeKit device."""
|
||||
|
||||
def __init__(self, hass, host, port, model, hkid, config_num, config):
|
||||
"""Initialise a generic HomeKit device."""
|
||||
# pylint: disable=import-error
|
||||
import homekit
|
||||
|
||||
_LOGGER.info("Setting up Homekit device %s", model)
|
||||
self.hass = hass
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.model = model
|
||||
self.hkid = hkid
|
||||
self.config_num = config_num
|
||||
self.config = config
|
||||
self.configurator = hass.components.configurator
|
||||
|
||||
data_dir = os.path.join(hass.config.path(), HOMEKIT_DIR)
|
||||
if not os.path.isdir(data_dir):
|
||||
os.mkdir(data_dir)
|
||||
|
||||
self.pairing_file = os.path.join(data_dir, 'hk-{}'.format(hkid))
|
||||
self.pairing_data = homekit.load_pairing(self.pairing_file)
|
||||
|
||||
# Monkey patch httpclient for increased compatibility
|
||||
# pylint: disable=protected-access
|
||||
http.client.HTTPConnection._send_output = homekit_http_send
|
||||
|
||||
self.conn = http.client.HTTPConnection(self.host, port=self.port)
|
||||
if self.pairing_data is not None:
|
||||
self.accessory_setup()
|
||||
else:
|
||||
self.configure()
|
||||
|
||||
def accessory_setup(self):
|
||||
"""Handle setup of a HomeKit accessory."""
|
||||
# pylint: disable=import-error
|
||||
import homekit
|
||||
self.controllerkey, self.accessorykey = \
|
||||
homekit.get_session_keys(self.conn, self.pairing_data)
|
||||
self.securecon = homekit.SecureHttp(self.conn.sock,
|
||||
self.accessorykey,
|
||||
self.controllerkey)
|
||||
response = self.securecon.get('/accessories')
|
||||
data = json.loads(response.read().decode())
|
||||
for accessory in data['accessories']:
|
||||
serial = get_serial(accessory)
|
||||
if serial in self.hass.data[KNOWN_ACCESSORIES]:
|
||||
continue
|
||||
self.hass.data[KNOWN_ACCESSORIES][serial] = self
|
||||
aid = accessory['aid']
|
||||
for service in accessory['services']:
|
||||
service_info = {'serial': serial,
|
||||
'aid': aid,
|
||||
'iid': service['iid']}
|
||||
devtype = homekit.ServicesTypes.get_short(service['type'])
|
||||
_LOGGER.debug("Found %s", devtype)
|
||||
component = HOMEKIT_ACCESSORY_DISPATCH.get(devtype, None)
|
||||
if component is not None:
|
||||
discovery.load_platform(self.hass, component, DOMAIN,
|
||||
service_info, self.config)
|
||||
|
||||
def device_config_callback(self, callback_data):
|
||||
"""Handle initial pairing."""
|
||||
# pylint: disable=import-error
|
||||
import homekit
|
||||
pairing_id = str(uuid.uuid4())
|
||||
code = callback_data.get('code').strip()
|
||||
self.pairing_data = homekit.perform_pair_setup(
|
||||
self.conn, code, pairing_id)
|
||||
if self.pairing_data is not None:
|
||||
homekit.save_pairing(self.pairing_file, self.pairing_data)
|
||||
self.accessory_setup()
|
||||
else:
|
||||
error_msg = "Unable to pair, please try again"
|
||||
_configurator = self.hass.data[DOMAIN+self.hkid]
|
||||
self.configurator.notify_errors(_configurator, error_msg)
|
||||
|
||||
def configure(self):
|
||||
"""Obtain the pairing code for a HomeKit device."""
|
||||
description = "Please enter the HomeKit code for your {}".format(
|
||||
self.model)
|
||||
self.hass.data[DOMAIN+self.hkid] = \
|
||||
self.configurator.request_config(self.model,
|
||||
self.device_config_callback,
|
||||
description=description,
|
||||
submit_caption="submit",
|
||||
fields=[{'id': 'code',
|
||||
'name': 'HomeKit code',
|
||||
'type': 'string'}])
|
||||
|
||||
|
||||
class HomeKitEntity(Entity):
|
||||
"""Representation of a Home Assistant HomeKit device."""
|
||||
|
||||
def __init__(self, accessory, devinfo):
|
||||
"""Initialise a generic HomeKit device."""
|
||||
self._name = accessory.model
|
||||
self._securecon = accessory.securecon
|
||||
self._aid = devinfo['aid']
|
||||
self._iid = devinfo['iid']
|
||||
self._address = "homekit-{}-{}".format(devinfo['serial'], self._iid)
|
||||
self._features = 0
|
||||
self._chars = {}
|
||||
|
||||
def update(self):
|
||||
"""Obtain a HomeKit device's state."""
|
||||
response = self._securecon.get('/accessories')
|
||||
data = json.loads(response.read().decode())
|
||||
for accessory in data['accessories']:
|
||||
if accessory['aid'] != self._aid:
|
||||
continue
|
||||
for service in accessory['services']:
|
||||
if service['iid'] != self._iid:
|
||||
continue
|
||||
self.update_characteristics(service['characteristics'])
|
||||
break
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this device."""
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return self._name
|
||||
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise a HomeKit device state with Home Assistant."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
# pylint: too-many-function-args
|
||||
def setup(hass, config):
|
||||
"""Set up for Homekit devices."""
|
||||
def discovery_dispatch(service, discovery_info):
|
||||
"""Dispatcher for Homekit discovery events."""
|
||||
# model, id
|
||||
host = discovery_info['host']
|
||||
port = discovery_info['port']
|
||||
model = discovery_info['properties']['md']
|
||||
hkid = discovery_info['properties']['id']
|
||||
config_num = int(discovery_info['properties']['c#'])
|
||||
|
||||
# Only register a device once, but rescan if the config has changed
|
||||
if hkid in hass.data[KNOWN_DEVICES]:
|
||||
device = hass.data[KNOWN_DEVICES][hkid]
|
||||
if config_num > device.config_num and \
|
||||
device.pairing_info is not None:
|
||||
device.accessory_setup()
|
||||
return
|
||||
|
||||
_LOGGER.debug('Discovered unique device %s', hkid)
|
||||
device = HKDevice(hass, host, port, model, hkid, config_num, config)
|
||||
hass.data[KNOWN_DEVICES][hkid] = device
|
||||
|
||||
hass.data[KNOWN_ACCESSORIES] = {}
|
||||
hass.data[KNOWN_DEVICES] = {}
|
||||
discovery.listen(hass, SERVICE_HOMEKIT, discovery_dispatch)
|
||||
return True
|
|
@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['pyhomematic==0.1.40']
|
||||
REQUIREMENTS = ['pyhomematic==0.1.41']
|
||||
DOMAIN = 'homematic'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -69,7 +69,8 @@ HM_DEVICE_TYPES = {
|
|||
'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor',
|
||||
'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch',
|
||||
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
|
||||
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat'],
|
||||
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat',
|
||||
'IPWeatherSensor'],
|
||||
DISCOVER_CLIMATE: [
|
||||
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
|
||||
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
|
||||
|
@ -78,7 +79,7 @@ HM_DEVICE_TYPES = {
|
|||
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
|
||||
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
|
||||
'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
|
||||
'WiredSensor', 'PresenceIP'],
|
||||
'WiredSensor', 'PresenceIP', 'IPWeatherSensor'],
|
||||
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'],
|
||||
DISCOVER_LOCKS: ['KeyMatic']
|
||||
}
|
||||
|
@ -89,7 +90,7 @@ HM_IGNORE_DISCOVERY_NODE = [
|
|||
]
|
||||
|
||||
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = {
|
||||
'ACTUAL_TEMPERATURE': ['IPAreaThermostat'],
|
||||
'ACTUAL_TEMPERATURE': ['IPAreaThermostat', 'IPWeatherSensor'],
|
||||
}
|
||||
|
||||
HM_ATTRIBUTE_SUPPORT = {
|
||||
|
|
|
@ -131,3 +131,9 @@ async def async_setup_entry(hass, entry):
|
|||
bridge = HueBridge(hass, entry, allow_unreachable, allow_groups)
|
||||
hass.data[DOMAIN][host] = bridge
|
||||
return await bridge.async_setup()
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
bridge = hass.data[DOMAIN].pop(entry.data['host'])
|
||||
return await bridge.async_reset()
|
||||
|
|
|
@ -30,6 +30,7 @@ class HueBridge(object):
|
|||
self.allow_groups = allow_groups
|
||||
self.available = True
|
||||
self.api = None
|
||||
self._cancel_retry_setup = None
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
|
@ -39,18 +40,17 @@ class HueBridge(object):
|
|||
async def async_setup(self, tries=0):
|
||||
"""Set up a phue bridge based on host parameter."""
|
||||
host = self.host
|
||||
hass = self.hass
|
||||
|
||||
try:
|
||||
self.api = await get_bridge(
|
||||
self.hass, host,
|
||||
self.config_entry.data['username']
|
||||
)
|
||||
hass, host, self.config_entry.data['username'])
|
||||
except AuthenticationRequired:
|
||||
# usernames can become invalid if hub is reset or user removed.
|
||||
# We are going to fail the config entry setup and initiate a new
|
||||
# linking procedure. When linking succeeds, it will remove the
|
||||
# old config entry.
|
||||
self.hass.async_add_job(self.hass.config_entries.flow.async_init(
|
||||
hass.async_add_job(hass.config_entries.flow.async_init(
|
||||
DOMAIN, source='import', data={
|
||||
'host': host,
|
||||
}
|
||||
|
@ -68,8 +68,8 @@ class HueBridge(object):
|
|||
# This feels hacky, we should find a better way to do this
|
||||
self.config_entry.state = config_entries.ENTRY_STATE_LOADED
|
||||
|
||||
# Unhandled edge case: cancel this if we discover bridge on new IP
|
||||
self.hass.helpers.event.async_call_later(retry_delay, retry_setup)
|
||||
self._cancel_retry_setup = hass.helpers.event.async_call_later(
|
||||
retry_delay, retry_setup)
|
||||
|
||||
return False
|
||||
|
||||
|
@ -78,16 +78,43 @@ class HueBridge(object):
|
|||
host)
|
||||
return False
|
||||
|
||||
self.hass.async_add_job(
|
||||
self.hass.helpers.discovery.async_load_platform(
|
||||
'light', DOMAIN, {'host': host}))
|
||||
hass.async_add_job(hass.config_entries.async_forward_entry_setup(
|
||||
self.config_entry, 'light'))
|
||||
|
||||
self.hass.services.async_register(
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
|
||||
schema=SCENE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
async def async_reset(self):
|
||||
"""Reset this bridge to default state.
|
||||
|
||||
Will cancel any scheduled setup retry and will unload
|
||||
the config entry.
|
||||
"""
|
||||
# The bridge can be in 3 states:
|
||||
# - Setup was successful, self.api is not None
|
||||
# - Authentication was wrong, self.api is None, not retrying setup.
|
||||
# - Host was down. self.api is None, we're retrying setup
|
||||
|
||||
# If we have a retry scheduled, we were never setup.
|
||||
if self._cancel_retry_setup is not None:
|
||||
self._cancel_retry_setup()
|
||||
self._cancel_retry_setup = None
|
||||
return True
|
||||
|
||||
# If the authentication was wrong.
|
||||
if self.api is None:
|
||||
return True
|
||||
|
||||
self.hass.services.async_remove(DOMAIN, SERVICE_HUE_SCENE)
|
||||
|
||||
# If setup was successful, we set api variable, forwarded entry and
|
||||
# register service
|
||||
return await self.hass.config_entries.async_forward_entry_unload(
|
||||
self.config_entry, 'light')
|
||||
|
||||
async def hue_activate_scene(self, call, updated=False):
|
||||
"""Service to call directly into bridge to set scenes."""
|
||||
group_name = call.data[ATTR_GROUP_NAME]
|
||||
|
|
|
@ -6,7 +6,7 @@ import os
|
|||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
|
@ -41,7 +41,7 @@ def _find_username_from_config(hass, filename):
|
|||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class HueFlowHandler(config_entries.ConfigFlowHandler):
|
||||
class HueFlowHandler(data_entry_flow.FlowHandler):
|
||||
"""Handle a Hue config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""IHC component.
|
||||
"""
|
||||
Support for IHC devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/ihc/
|
||||
|
@ -6,18 +7,18 @@ https://home-assistant.io/components/ihc/
|
|||
import logging
|
||||
import os.path
|
||||
import xml.etree.ElementTree
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.ihc.const import (
|
||||
ATTR_IHC_ID, ATTR_VALUE, CONF_INFO, CONF_AUTOSETUP,
|
||||
CONF_BINARY_SENSOR, CONF_LIGHT, CONF_SENSOR, CONF_SWITCH,
|
||||
CONF_XPATH, CONF_NODE, CONF_DIMMABLE, CONF_INVERTING,
|
||||
SERVICE_SET_RUNTIME_VALUE_BOOL, SERVICE_SET_RUNTIME_VALUE_INT,
|
||||
SERVICE_SET_RUNTIME_VALUE_FLOAT)
|
||||
ATTR_IHC_ID, ATTR_VALUE, CONF_AUTOSETUP, CONF_BINARY_SENSOR, CONF_DIMMABLE,
|
||||
CONF_INFO, CONF_INVERTING, CONF_LIGHT, CONF_NODE, CONF_SENSOR, CONF_SWITCH,
|
||||
CONF_XPATH, SERVICE_SET_RUNTIME_VALUE_BOOL,
|
||||
SERVICE_SET_RUNTIME_VALUE_FLOAT, SERVICE_SET_RUNTIME_VALUE_INT)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
CONF_URL, CONF_USERNAME, CONF_PASSWORD, CONF_ID, CONF_NAME,
|
||||
CONF_UNIT_OF_MEASUREMENT, CONF_TYPE, TEMP_CELSIUS)
|
||||
CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_URL, CONF_USERNAME, TEMP_CELSIUS)
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
@ -36,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_AUTOSETUP, default=True): cv.boolean,
|
||||
vol.Optional(CONF_INFO, default=True): cv.boolean
|
||||
vol.Optional(CONF_INFO, default=True): cv.boolean,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
@ -97,7 +98,7 @@ IHC_PLATFORMS = ('binary_sensor', 'light', 'sensor', 'switch')
|
|||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the IHC component."""
|
||||
"""Set up the IHC component."""
|
||||
from ihcsdk.ihccontroller import IHCController
|
||||
conf = config[DOMAIN]
|
||||
url = conf[CONF_URL]
|
||||
|
@ -106,7 +107,7 @@ def setup(hass, config):
|
|||
ihc_controller = IHCController(url, username, password)
|
||||
|
||||
if not ihc_controller.authenticate():
|
||||
_LOGGER.error("Unable to authenticate on ihc controller.")
|
||||
_LOGGER.error("Unable to authenticate on IHC controller")
|
||||
return False
|
||||
|
||||
if (conf[CONF_AUTOSETUP] and
|
||||
|
@ -125,7 +126,7 @@ def autosetup_ihc_products(hass: HomeAssistantType, config, ihc_controller):
|
|||
"""Auto setup of IHC products from the ihc project file."""
|
||||
project_xml = ihc_controller.get_project()
|
||||
if not project_xml:
|
||||
_LOGGER.error("Unable to read project from ihc controller.")
|
||||
_LOGGER.error("Unable to read project from ICH controller")
|
||||
return False
|
||||
project = xml.etree.ElementTree.fromstring(project_xml)
|
||||
|
||||
|
@ -150,7 +151,7 @@ def autosetup_ihc_products(hass: HomeAssistantType, config, ihc_controller):
|
|||
|
||||
|
||||
def get_discovery_info(component_setup, groups):
|
||||
"""Get discovery info for specified component."""
|
||||
"""Get discovery info for specified IHC component."""
|
||||
discovery_data = {}
|
||||
for group in groups:
|
||||
groupname = group.attrib['name']
|
||||
|
@ -173,7 +174,7 @@ def get_discovery_info(component_setup, groups):
|
|||
|
||||
|
||||
def setup_service_functions(hass: HomeAssistantType, ihc_controller):
|
||||
"""Setup the ihc service functions."""
|
||||
"""Setup the IHC service functions."""
|
||||
def set_runtime_value_bool(call):
|
||||
"""Set a IHC runtime bool value service function."""
|
||||
ihc_id = call.data[ATTR_IHC_ID]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
"""Implements a base class for all IHC devices."""
|
||||
"""Implementation of a base class for all IHC devices."""
|
||||
import asyncio
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
|
@ -6,7 +6,7 @@ from homeassistant.helpers.entity import Entity
|
|||
|
||||
|
||||
class IHCDevice(Entity):
|
||||
"""Base class for all ihc devices.
|
||||
"""Base class for all IHC devices.
|
||||
|
||||
All IHC devices have an associated IHC resource. IHCDevice handled the
|
||||
registration of the IHC controller callback when the IHC resource changes.
|
||||
|
@ -31,13 +31,13 @@ class IHCDevice(Entity):
|
|||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Add callback for ihc changes."""
|
||||
"""Add callback for IHC changes."""
|
||||
self.ihc_controller.add_notify_event(
|
||||
self.ihc_id, self.on_ihc_change, True)
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""No polling needed for ihc devices."""
|
||||
"""No polling needed for IHC devices."""
|
||||
return False
|
||||
|
||||
@property
|
||||
|
@ -58,7 +58,7 @@ class IHCDevice(Entity):
|
|||
}
|
||||
|
||||
def on_ihc_change(self, ihc_id, value):
|
||||
"""Callback when ihc resource changes.
|
||||
"""Callback when IHC resource changes.
|
||||
|
||||
Derived classes must overwrite this to do device specific stuff.
|
||||
"""
|
||||
|
|
|
@ -334,7 +334,7 @@ class SetIntentHandler(intent.IntentHandler):
|
|||
|
||||
async def async_setup(hass, config):
|
||||
"""Expose light control via state machine and services."""
|
||||
component = EntityComponent(
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS)
|
||||
await component.async_setup(config)
|
||||
|
||||
|
@ -388,6 +388,16 @@ async def async_setup(hass, config):
|
|||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Setup a config entry."""
|
||||
return await hass.data[DOMAIN].async_setup_entry(entry)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||
|
||||
|
||||
class Profiles:
|
||||
"""Representation of available color profiles."""
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/light.abode/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from math import ceil
|
||||
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_HS_COLOR,
|
||||
|
@ -51,7 +51,9 @@ class AbodeLight(AbodeDevice, Light):
|
|||
*kwargs[ATTR_HS_COLOR]))
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable:
|
||||
self._device.set_level(kwargs[ATTR_BRIGHTNESS])
|
||||
# Convert HASS brightness (0-255) to Abode brightness (0-99)
|
||||
# If 100 is sent to Abode, response is 99 causing an error
|
||||
self._device.set_level(ceil(kwargs[ATTR_BRIGHTNESS] * 99 / 255.0))
|
||||
else:
|
||||
self._device.switch_on()
|
||||
|
||||
|
@ -68,7 +70,12 @@ class AbodeLight(AbodeDevice, Light):
|
|||
def brightness(self):
|
||||
"""Return the brightness of the light."""
|
||||
if self._device.is_dimmable and self._device.has_brightness:
|
||||
return self._device.brightness
|
||||
brightness = int(self._device.brightness)
|
||||
# Abode returns 100 during device initialization and device refresh
|
||||
if brightness == 100:
|
||||
return 255
|
||||
# Convert Abode brightness (0-99) to HASS brightness (0-255)
|
||||
return ceil(brightness * 255 / 99.0)
|
||||
|
||||
@property
|
||||
def hs_color(self):
|
||||
|
|
168
homeassistant/components/light/eufy.py
Normal file
168
homeassistant/components/light/eufy.py
Normal file
|
@ -0,0 +1,168 @@
|
|||
"""
|
||||
Support for Eufy lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.eufy/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light)
|
||||
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from homeassistant.util.color import (
|
||||
color_temperature_mired_to_kelvin as mired_to_kelvin,
|
||||
color_temperature_kelvin_to_mired as kelvin_to_mired)
|
||||
|
||||
DEPENDENCIES = ['eufy']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EUFY_MAX_KELVIN = 6500
|
||||
EUFY_MIN_KELVIN = 2700
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Eufy bulbs."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
add_devices([EufyLight(discovery_info)], True)
|
||||
|
||||
|
||||
class EufyLight(Light):
|
||||
"""Representation of a Eufy light."""
|
||||
|
||||
def __init__(self, device):
|
||||
"""Initialize the light."""
|
||||
# pylint: disable=import-error
|
||||
import lakeside
|
||||
|
||||
self._temp = None
|
||||
self._brightness = None
|
||||
self._hs = None
|
||||
self._state = None
|
||||
self._name = device['name']
|
||||
self._address = device['address']
|
||||
self._code = device['code']
|
||||
self._type = device['type']
|
||||
self._bulb = lakeside.bulb(self._address, self._code, self._type)
|
||||
self._colormode = False
|
||||
if self._type == "T1011":
|
||||
self._features = SUPPORT_BRIGHTNESS
|
||||
elif self._type == "T1012":
|
||||
self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP
|
||||
elif self._type == "T1013":
|
||||
self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | \
|
||||
SUPPORT_COLOR
|
||||
self._bulb.connect()
|
||||
|
||||
def update(self):
|
||||
"""Synchronise state from the bulb."""
|
||||
self._bulb.update()
|
||||
self._brightness = self._bulb.brightness
|
||||
self._temp = self._bulb.temperature
|
||||
if self._bulb.colors:
|
||||
self._colormode = True
|
||||
self._hs = color_util.color_RGB_to_hs(*self._bulb.colors)
|
||||
else:
|
||||
self._colormode = False
|
||||
self._state = self._bulb.power
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this light."""
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return int(self._brightness * 255 / 100)
|
||||
|
||||
@property
|
||||
def min_mireds(self):
|
||||
"""Return minimum supported color temperature."""
|
||||
return kelvin_to_mired(EUFY_MAX_KELVIN)
|
||||
|
||||
@property
|
||||
def max_mireds(self):
|
||||
"""Return maximu supported color temperature."""
|
||||
return kelvin_to_mired(EUFY_MIN_KELVIN)
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the color temperature of this light."""
|
||||
temp_in_k = int(EUFY_MIN_KELVIN + (self._temp *
|
||||
(EUFY_MAX_KELVIN - EUFY_MIN_KELVIN)
|
||||
/ 100))
|
||||
return kelvin_to_mired(temp_in_k)
|
||||
|
||||
@property
|
||||
def hs_color(self):
|
||||
"""Return the color of this light."""
|
||||
if not self._colormode:
|
||||
return None
|
||||
return self._hs
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return self._features
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the specified light on."""
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
colortemp = kwargs.get(ATTR_COLOR_TEMP)
|
||||
# pylint: disable=invalid-name
|
||||
hs = kwargs.get(ATTR_HS_COLOR)
|
||||
|
||||
if brightness is not None:
|
||||
brightness = int(brightness * 100 / 255)
|
||||
else:
|
||||
brightness = max(1, self._brightness)
|
||||
|
||||
if colortemp is not None:
|
||||
self._colormode = False
|
||||
temp_in_k = mired_to_kelvin(colortemp)
|
||||
relative_temp = temp_in_k - EUFY_MIN_KELVIN
|
||||
temp = int(relative_temp * 100 /
|
||||
(EUFY_MAX_KELVIN - EUFY_MIN_KELVIN))
|
||||
else:
|
||||
temp = None
|
||||
|
||||
if hs is not None:
|
||||
rgb = color_util.color_hsv_to_RGB(
|
||||
hs[0], hs[1], brightness / 255 * 100)
|
||||
self._colormode = True
|
||||
elif self._colormode:
|
||||
rgb = color_util.color_hsv_to_RGB(
|
||||
self._hs[0], self._hs[1], brightness / 255 * 100)
|
||||
else:
|
||||
rgb = None
|
||||
|
||||
try:
|
||||
self._bulb.set_state(power=True, brightness=brightness,
|
||||
temperature=temp, colors=rgb)
|
||||
except BrokenPipeError:
|
||||
self._bulb.connect()
|
||||
self._bulb.set_state(power=True, brightness=brightness,
|
||||
temperature=temp, colors=rgb)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the specified light off."""
|
||||
try:
|
||||
self._bulb.set_state(power=False)
|
||||
except BrokenPipeError:
|
||||
self._bulb.connect()
|
||||
self._bulb.set_state(power=False)
|
|
@ -34,6 +34,7 @@ class HiveDeviceLight(Light):
|
|||
self.device_type = hivedevice["HA_DeviceType"]
|
||||
self.light_device_type = hivedevice["Hive_Light_DeviceType"]
|
||||
self.session = hivesession
|
||||
self.attributes = {}
|
||||
self.data_updatesource = '{}.{}'.format(self.device_type,
|
||||
self.node_id)
|
||||
self.session.entities.append(self)
|
||||
|
@ -48,6 +49,11 @@ class HiveDeviceLight(Light):
|
|||
"""Return the display name of this light."""
|
||||
return self.node_name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Show Device Attributes."""
|
||||
return self.attributes
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Brightness of the light (an integer in the range 1-255)."""
|
||||
|
@ -136,3 +142,5 @@ class HiveDeviceLight(Light):
|
|||
def update(self):
|
||||
"""Update all Node data from Hive."""
|
||||
self.session.core.update_data(self.node_id)
|
||||
self.attributes = self.session.attributes.state_attributes(
|
||||
self.node_id)
|
||||
|
|
134
homeassistant/components/light/homekit_controller.py
Normal file
134
homeassistant/components/light/homekit_controller.py
Normal file
|
@ -0,0 +1,134 @@
|
|||
"""
|
||||
Support for Homekit lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.homekit_controller/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from homeassistant.components.homekit_controller import (
|
||||
HomeKitEntity, KNOWN_ACCESSORIES)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light)
|
||||
|
||||
DEPENDENCIES = ['homekit_controller']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Homekit lighting."""
|
||||
if discovery_info is not None:
|
||||
accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']]
|
||||
add_devices([HomeKitLight(accessory, discovery_info)], True)
|
||||
|
||||
|
||||
class HomeKitLight(HomeKitEntity, Light):
|
||||
"""Representation of a Homekit light."""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""Initialise the light."""
|
||||
super().__init__(*args)
|
||||
self._on = None
|
||||
self._brightness = None
|
||||
self._color_temperature = None
|
||||
self._hue = None
|
||||
self._saturation = None
|
||||
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise light state with Home Assistant."""
|
||||
# pylint: disable=import-error
|
||||
import homekit
|
||||
|
||||
for characteristic in characteristics:
|
||||
ctype = characteristic['type']
|
||||
ctype = homekit.CharacteristicsTypes.get_short(ctype)
|
||||
if ctype == "on":
|
||||
self._chars['on'] = characteristic['iid']
|
||||
self._on = characteristic['value']
|
||||
elif ctype == 'brightness':
|
||||
self._chars['brightness'] = characteristic['iid']
|
||||
self._features |= SUPPORT_BRIGHTNESS
|
||||
self._brightness = characteristic['value']
|
||||
elif ctype == 'color-temperature':
|
||||
self._chars['color_temperature'] = characteristic['iid']
|
||||
self._features |= SUPPORT_COLOR_TEMP
|
||||
self._color_temperature = characteristic['value']
|
||||
elif ctype == "hue":
|
||||
self._chars['hue'] = characteristic['iid']
|
||||
self._features |= SUPPORT_COLOR
|
||||
self._hue = characteristic['value']
|
||||
elif ctype == "saturation":
|
||||
self._chars['saturation'] = characteristic['iid']
|
||||
self._features |= SUPPORT_COLOR
|
||||
self._saturation = characteristic['value']
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self._on
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
if self._features & SUPPORT_BRIGHTNESS:
|
||||
return self._brightness * 255 / 100
|
||||
return None
|
||||
|
||||
@property
|
||||
def hs_color(self):
|
||||
"""Return the color property."""
|
||||
if self._features & SUPPORT_COLOR:
|
||||
return (self._hue, self._saturation)
|
||||
return None
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the color temperature."""
|
||||
if self._features & SUPPORT_COLOR_TEMP:
|
||||
return self._color_temperature
|
||||
return None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return self._features
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the specified light on."""
|
||||
hs_color = kwargs.get(ATTR_HS_COLOR)
|
||||
temperature = kwargs.get(ATTR_COLOR_TEMP)
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
|
||||
characteristics = []
|
||||
if hs_color is not None:
|
||||
characteristics.append({'aid': self._aid,
|
||||
'iid': self._chars['hue'],
|
||||
'value': hs_color[0]})
|
||||
characteristics.append({'aid': self._aid,
|
||||
'iid': self._chars['saturation'],
|
||||
'value': hs_color[1]})
|
||||
if brightness is not None:
|
||||
characteristics.append({'aid': self._aid,
|
||||
'iid': self._chars['brightness'],
|
||||
'value': int(brightness * 100 / 255)})
|
||||
|
||||
if temperature is not None:
|
||||
characteristics.append({'aid': self._aid,
|
||||
'iid': self._chars['color-temperature'],
|
||||
'value': int(temperature)})
|
||||
characteristics.append({'aid': self._aid,
|
||||
'iid': self._chars['on'],
|
||||
'value': True})
|
||||
body = json.dumps({'characteristics': characteristics})
|
||||
self._securecon.put('/characteristics', body)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the specified light off."""
|
||||
characteristics = [{'aid': self._aid,
|
||||
'iid': self._chars['on'],
|
||||
'value': False}]
|
||||
body = json.dumps({'characteristics': characteristics})
|
||||
self._securecon.put('/characteristics', body)
|
|
@ -49,11 +49,17 @@ GROUP_MIN_API_VERSION = (1, 13, 0)
|
|||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the Hue lights."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
"""Old way of setting up Hue lights.
|
||||
|
||||
bridge = hass.data[hue.DOMAIN][discovery_info['host']]
|
||||
Can only be called when a user accidentally mentions hue platform in their
|
||||
config. But even in that case it would have been ignored.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the Hue lights from a config entry."""
|
||||
bridge = hass.data[hue.DOMAIN][config_entry.data['host']]
|
||||
cur_lights = {}
|
||||
cur_groups = {}
|
||||
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
"""
|
||||
Support for Nanoleaf Aurora platform.
|
||||
|
||||
Based in large parts upon Software-2's ha-aurora and fully
|
||||
reliant on Software-2's nanoleaf-aurora Python Library, see
|
||||
https://github.com/software-2/ha-aurora as well as
|
||||
https://github.com/software-2/nanoleaf
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.nanoleaf_aurora/
|
||||
"""
|
||||
|
@ -15,9 +10,9 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR,
|
||||
SUPPORT_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
|
||||
SUPPORT_COLOR, PLATFORM_SCHEMA, Light)
|
||||
from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_NAME
|
||||
PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP,
|
||||
SUPPORT_EFFECT, Light)
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import color as color_util
|
||||
from homeassistant.util.color import \
|
||||
|
@ -25,20 +20,24 @@ from homeassistant.util.color import \
|
|||
|
||||
REQUIREMENTS = ['nanoleaf==0.4.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Aurora'
|
||||
|
||||
ICON = 'mdi:triangle-outline'
|
||||
|
||||
SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
|
||||
SUPPORT_COLOR)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_TOKEN): cv.string,
|
||||
vol.Optional(CONF_NAME, default='Aurora'): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Nanoleaf Aurora device."""
|
||||
"""Set up the Nanoleaf Aurora device."""
|
||||
import nanoleaf
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME)
|
||||
|
@ -47,8 +46,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
aurora_light.hass_name = name
|
||||
|
||||
if aurora_light.on is None:
|
||||
_LOGGER.error("Could not connect to \
|
||||
Nanoleaf Aurora: %s on %s", name, host)
|
||||
_LOGGER.error(
|
||||
"Could not connect to Nanoleaf Aurora: %s on %s", name, host)
|
||||
return
|
||||
|
||||
add_devices([AuroraLight(aurora_light)], True)
|
||||
|
||||
|
||||
|
@ -56,7 +57,7 @@ class AuroraLight(Light):
|
|||
"""Representation of a Nanoleaf Aurora."""
|
||||
|
||||
def __init__(self, light):
|
||||
"""Initialize an Aurora."""
|
||||
"""Initialize an Aurora light."""
|
||||
self._brightness = None
|
||||
self._color_temp = None
|
||||
self._effect = None
|
||||
|
@ -99,7 +100,7 @@ class AuroraLight(Light):
|
|||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return "mdi:triangle-outline"
|
||||
return ICON
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
@ -141,10 +142,7 @@ class AuroraLight(Light):
|
|||
self._light.on = False
|
||||
|
||||
def update(self):
|
||||
"""Fetch new state data for this light.
|
||||
|
||||
This is the only method that should fetch new data for Home Assistant.
|
||||
"""
|
||||
"""Fetch new state data for this light."""
|
||||
self._brightness = self._light.brightness
|
||||
self._color_temp = self._light.color_temperature
|
||||
self._effect = self._light.effect
|
||||
|
|
|
@ -27,9 +27,9 @@ class QSLight(QSToggleEntity, Light):
|
|||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light (0-255)."""
|
||||
return self._qsusb[self.qsid, 1] if self._dim else None
|
||||
return self.device.value if self.device.is_dimmer else None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_BRIGHTNESS if self._dim else 0
|
||||
return SUPPORT_BRIGHTNESS if self.device.is_dimmer else 0
|
||||
|
|
|
@ -24,6 +24,14 @@ REQUIREMENTS = ['yeelight==0.4.0']
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LEGACY_DEVICE_TYPE_MAP = {
|
||||
'color1': 'rgb',
|
||||
'mono1': 'white',
|
||||
'strip1': 'strip',
|
||||
'bslamp1': 'bedside',
|
||||
'ceiling1': 'ceiling',
|
||||
}
|
||||
|
||||
CONF_TRANSITION = 'transition'
|
||||
DEFAULT_TRANSITION = 350
|
||||
|
||||
|
@ -122,8 +130,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
if discovery_info is not None:
|
||||
_LOGGER.debug("Adding autodetected %s", discovery_info['hostname'])
|
||||
|
||||
device_type = discovery_info['device_type']
|
||||
device_type = LEGACY_DEVICE_TYPE_MAP.get(device_type, device_type)
|
||||
|
||||
# Not using hostname, as it seems to vary.
|
||||
name = "yeelight_%s_%s" % (discovery_info['device_type'],
|
||||
name = "yeelight_%s_%s" % (device_type,
|
||||
discovery_info['properties']['mac'])
|
||||
device = {'name': name, 'ipaddr': discovery_info['host']}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ class BMWLock(LockDevice):
|
|||
self._vehicle = vehicle
|
||||
self._attribute = attribute
|
||||
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
|
||||
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
|
||||
self._sensor_name = sensor_name
|
||||
self._state = None
|
||||
|
||||
|
@ -49,6 +50,11 @@ class BMWLock(LockDevice):
|
|||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of the lock."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the lock."""
|
||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.const import CONF_HOST
|
|||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['pylutron-caseta==0.3.0']
|
||||
REQUIREMENTS = ['pylutron-caseta==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -22,12 +22,22 @@ _LOGGER = logging.getLogger(__name__)
|
|||
DEFAULT_PORT = 62910
|
||||
DOMAIN = 'maxcube'
|
||||
|
||||
MAXCUBE_HANDLE = 'maxcube'
|
||||
DATA_KEY = 'maxcube'
|
||||
|
||||
NOTIFICATION_ID = 'maxcube_notification'
|
||||
NOTIFICATION_TITLE = 'Max!Cube gateway setup'
|
||||
|
||||
CONF_GATEWAYS = 'gateways'
|
||||
|
||||
CONFIG_GATEWAY = vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Required(CONF_GATEWAYS, default={}):
|
||||
vol.All(cv.ensure_list, [CONFIG_GATEWAY])
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
@ -36,19 +46,31 @@ def setup(hass, config):
|
|||
"""Establish connection to MAX! Cube."""
|
||||
from maxcube.connection import MaxCubeConnection
|
||||
from maxcube.cube import MaxCube
|
||||
if DATA_KEY not in hass.data:
|
||||
hass.data[DATA_KEY] = {}
|
||||
|
||||
host = config.get(DOMAIN).get(CONF_HOST)
|
||||
port = config.get(DOMAIN).get(CONF_PORT)
|
||||
connection_failed = 0
|
||||
gateways = config[DOMAIN][CONF_GATEWAYS]
|
||||
for gateway in gateways:
|
||||
host = gateway[CONF_HOST]
|
||||
port = gateway[CONF_PORT]
|
||||
|
||||
try:
|
||||
cube = MaxCube(MaxCubeConnection(host, port))
|
||||
except timeout:
|
||||
_LOGGER.error("Connection to Max!Cube could not be established")
|
||||
cube = None
|
||||
try:
|
||||
cube = MaxCube(MaxCubeConnection(host, port))
|
||||
hass.data[DATA_KEY][host] = MaxCubeHandle(cube)
|
||||
except timeout as ex:
|
||||
_LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex))
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart Home Assistant after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
connection_failed += 1
|
||||
|
||||
if connection_failed >= len(gateways):
|
||||
return False
|
||||
|
||||
hass.data[MAXCUBE_HANDLE] = MaxCubeHandle(cube)
|
||||
|
||||
load_platform(hass, 'climate', DOMAIN)
|
||||
load_platform(hass, 'binary_sensor', DOMAIN)
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
|
|||
SERVICE_PLAY_MEDIA)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['youtube_dl==2018.04.03']
|
||||
REQUIREMENTS = ['youtube_dl==2018.04.16']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
213
homeassistant/components/media_player/blackbird.py
Normal file
213
homeassistant/components/media_player/blackbird.py
Normal file
|
@ -0,0 +1,213 @@
|
|||
"""
|
||||
Support for interfacing with Monoprice Blackbird 4k 8x8 HDBaseT Matrix.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.blackbird
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
DOMAIN, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE,
|
||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_NAME, CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyblackbird==0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_BLACKBIRD = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
|
||||
SUPPORT_SELECT_SOURCE
|
||||
|
||||
ZONE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
SOURCE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
CONF_ZONES = 'zones'
|
||||
CONF_SOURCES = 'sources'
|
||||
CONF_TYPE = 'type'
|
||||
|
||||
DATA_BLACKBIRD = 'blackbird'
|
||||
|
||||
SERVICE_SETALLZONES = 'blackbird_set_all_zones'
|
||||
ATTR_SOURCE = 'source'
|
||||
|
||||
BLACKBIRD_SETALLZONES_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_SOURCE): cv.string
|
||||
})
|
||||
|
||||
|
||||
# Valid zone ids: 1-8
|
||||
ZONE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8))
|
||||
|
||||
# Valid source ids: 1-8
|
||||
SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8))
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_TYPE): vol.In(['serial', 'socket']),
|
||||
vol.Optional(CONF_PORT): cv.string,
|
||||
vol.Optional(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}),
|
||||
vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}),
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Monoprice Blackbird 4k 8x8 HDBaseT Matrix platform."""
|
||||
port = config.get(CONF_PORT)
|
||||
host = config.get(CONF_HOST)
|
||||
device_type = config.get(CONF_TYPE)
|
||||
|
||||
import socket
|
||||
from pyblackbird import get_blackbird
|
||||
from serial import SerialException
|
||||
|
||||
if device_type == 'serial':
|
||||
if port is None:
|
||||
_LOGGER.error("No port configured")
|
||||
return
|
||||
try:
|
||||
blackbird = get_blackbird(port)
|
||||
except SerialException:
|
||||
_LOGGER.error("Error connecting to the Blackbird controller")
|
||||
return
|
||||
|
||||
elif device_type == 'socket':
|
||||
try:
|
||||
if host is None:
|
||||
_LOGGER.error("No host configured")
|
||||
return
|
||||
blackbird = get_blackbird(host, False)
|
||||
except socket.timeout:
|
||||
_LOGGER.error("Error connecting to the Blackbird controller")
|
||||
return
|
||||
|
||||
else:
|
||||
_LOGGER.error("Incorrect device type specified")
|
||||
return
|
||||
|
||||
sources = {source_id: extra[CONF_NAME] for source_id, extra
|
||||
in config[CONF_SOURCES].items()}
|
||||
|
||||
hass.data[DATA_BLACKBIRD] = []
|
||||
for zone_id, extra in config[CONF_ZONES].items():
|
||||
_LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME])
|
||||
hass.data[DATA_BLACKBIRD].append(BlackbirdZone(
|
||||
blackbird, sources, zone_id, extra[CONF_NAME]))
|
||||
|
||||
add_devices(hass.data[DATA_BLACKBIRD], True)
|
||||
|
||||
def service_handle(service):
|
||||
"""Handle for services."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
source = service.data.get(ATTR_SOURCE)
|
||||
if entity_ids:
|
||||
devices = [device for device in hass.data[DATA_BLACKBIRD]
|
||||
if device.entity_id in entity_ids]
|
||||
|
||||
else:
|
||||
devices = hass.data[DATA_BLACKBIRD]
|
||||
|
||||
for device in devices:
|
||||
if service.service == SERVICE_SETALLZONES:
|
||||
device.set_all_zones(source)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SETALLZONES, service_handle,
|
||||
schema=BLACKBIRD_SETALLZONES_SCHEMA)
|
||||
|
||||
|
||||
class BlackbirdZone(MediaPlayerDevice):
|
||||
"""Representation of a Blackbird matrix zone."""
|
||||
|
||||
def __init__(self, blackbird, sources, zone_id, zone_name):
|
||||
"""Initialize new zone."""
|
||||
self._blackbird = blackbird
|
||||
# dict source_id -> source name
|
||||
self._source_id_name = sources
|
||||
# dict source name -> source_id
|
||||
self._source_name_id = {v: k for k, v in sources.items()}
|
||||
# ordered list of all source names
|
||||
self._source_names = sorted(self._source_name_id.keys(),
|
||||
key=lambda v: self._source_name_id[v])
|
||||
self._zone_id = zone_id
|
||||
self._name = zone_name
|
||||
self._state = None
|
||||
self._source = None
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
state = self._blackbird.zone_status(self._zone_id)
|
||||
if not state:
|
||||
return False
|
||||
self._state = STATE_ON if state.power else STATE_OFF
|
||||
idx = state.av
|
||||
if idx in self._source_id_name:
|
||||
self._source = self._source_id_name[idx]
|
||||
else:
|
||||
self._source = None
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the zone."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the zone."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return flag of media commands that are supported."""
|
||||
return SUPPORT_BLACKBIRD
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Return the current source as media title."""
|
||||
return self._source
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Return the current input source of the device."""
|
||||
return self._source
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
return self._source_names
|
||||
|
||||
def set_all_zones(self, source):
|
||||
"""Set all zones to one source."""
|
||||
_LOGGER.debug("Setting all zones")
|
||||
if source not in self._source_name_id:
|
||||
return
|
||||
idx = self._source_name_id[source]
|
||||
_LOGGER.debug("Setting all zones source to %s", idx)
|
||||
self._blackbird.set_all_zone_source(idx)
|
||||
|
||||
def select_source(self, source):
|
||||
"""Set input source."""
|
||||
if source not in self._source_name_id:
|
||||
return
|
||||
idx = self._source_name_id[source]
|
||||
_LOGGER.debug("Setting zone %d source to %s", self._zone_id, idx)
|
||||
self._blackbird.set_zone_source(self._zone_id, idx)
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
_LOGGER.debug("Turning zone %d on", self._zone_id)
|
||||
self._blackbird.set_zone_power(self._zone_id, True)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the media player off."""
|
||||
_LOGGER.debug("Turning zone %d off", self._zone_id)
|
||||
self._blackbird.set_zone_power(self._zone_id, False)
|
|
@ -37,30 +37,30 @@ REQUIREMENTS = ['xmltodict==0.11.0']
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_GROUPED = 'grouped'
|
||||
|
||||
ATTR_MASTER = 'master'
|
||||
|
||||
SERVICE_JOIN = 'bluesound_join'
|
||||
SERVICE_UNJOIN = 'bluesound_unjoin'
|
||||
SERVICE_SET_TIMER = 'bluesound_set_sleep_timer'
|
||||
SERVICE_CLEAR_TIMER = 'bluesound_clear_sleep_timer'
|
||||
|
||||
DATA_BLUESOUND = 'bluesound'
|
||||
DEFAULT_PORT = 11000
|
||||
|
||||
SYNC_STATUS_INTERVAL = timedelta(minutes=5)
|
||||
UPDATE_CAPTURE_INTERVAL = timedelta(minutes=30)
|
||||
UPDATE_SERVICES_INTERVAL = timedelta(minutes=30)
|
||||
UPDATE_PRESETS_INTERVAL = timedelta(minutes=30)
|
||||
NODE_OFFLINE_CHECK_TIMEOUT = 180
|
||||
NODE_RETRY_INITIATION = timedelta(minutes=3)
|
||||
|
||||
SERVICE_CLEAR_TIMER = 'bluesound_clear_sleep_timer'
|
||||
SERVICE_JOIN = 'bluesound_join'
|
||||
SERVICE_SET_TIMER = 'bluesound_set_sleep_timer'
|
||||
SERVICE_UNJOIN = 'bluesound_unjoin'
|
||||
STATE_GROUPED = 'grouped'
|
||||
SYNC_STATUS_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
UPDATE_CAPTURE_INTERVAL = timedelta(minutes=30)
|
||||
UPDATE_PRESETS_INTERVAL = timedelta(minutes=30)
|
||||
UPDATE_SERVICES_INTERVAL = timedelta(minutes=30)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
}])
|
||||
})
|
||||
|
||||
|
@ -131,8 +131,8 @@ def _add_player(hass, async_add_devices, host, port=None, name=None):
|
|||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_player)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Bluesound platforms."""
|
||||
if DATA_BLUESOUND not in hass.data:
|
||||
hass.data[DATA_BLUESOUND] = []
|
||||
|
@ -202,6 +202,9 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
if self.port is None:
|
||||
self.port = DEFAULT_PORT
|
||||
|
||||
class _TimeoutException(Exception):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _try_get_index(string, search_string):
|
||||
"""Get the index."""
|
||||
|
@ -258,7 +261,8 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
while True:
|
||||
await self.async_update_status()
|
||||
|
||||
except (asyncio.TimeoutError, ClientError):
|
||||
except (asyncio.TimeoutError, ClientError,
|
||||
BluesoundPlayer._TimeoutException):
|
||||
_LOGGER.info("Node %s is offline, retrying later", self._name)
|
||||
await asyncio.sleep(
|
||||
NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop)
|
||||
|
@ -293,8 +297,8 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
self._retry_remove = async_track_time_interval(
|
||||
self._hass, self.async_init, NODE_RETRY_INITIATION)
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected when initiating error in %s",
|
||||
self.host)
|
||||
_LOGGER.exception(
|
||||
"Unexpected when initiating error in %s", self.host)
|
||||
raise
|
||||
|
||||
async def async_update(self):
|
||||
|
@ -307,8 +311,8 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
await self.async_update_captures()
|
||||
await self.async_update_services()
|
||||
|
||||
async def send_bluesound_command(self, method, raise_timeout=False,
|
||||
allow_offline=False):
|
||||
async def send_bluesound_command(
|
||||
self, method, raise_timeout=False, allow_offline=False):
|
||||
"""Send command to the player."""
|
||||
import xmltodict
|
||||
|
||||
|
@ -321,6 +325,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
|
||||
_LOGGER.debug("Calling URL: %s", url)
|
||||
response = None
|
||||
|
||||
try:
|
||||
websession = async_get_clientsession(self._hass)
|
||||
with async_timeout.timeout(10, loop=self._hass.loop):
|
||||
|
@ -332,6 +337,9 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
data = None
|
||||
else:
|
||||
data = xmltodict.parse(result)
|
||||
elif response.status == 595:
|
||||
_LOGGER.info("Status 595 returned, treating as timeout")
|
||||
raise BluesoundPlayer._TimeoutException()
|
||||
else:
|
||||
_LOGGER.error("Error %s on %s", response.status, url)
|
||||
return None
|
||||
|
@ -366,13 +374,9 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
|
||||
with async_timeout.timeout(125, loop=self._hass.loop):
|
||||
response = await self._polling_session.get(
|
||||
url,
|
||||
headers={CONNECTION: KEEP_ALIVE})
|
||||
url, headers={CONNECTION: KEEP_ALIVE})
|
||||
|
||||
if response.status != 200:
|
||||
_LOGGER.error("Error %s on %s. Trying one more time.",
|
||||
response.status, url)
|
||||
else:
|
||||
if response.status == 200:
|
||||
result = await response.text()
|
||||
self._is_online = True
|
||||
self._last_status_update = dt_util.utcnow()
|
||||
|
@ -380,8 +384,8 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
|
||||
group_name = self._status.get('groupName', None)
|
||||
if group_name != self._group_name:
|
||||
_LOGGER.debug('Group name change detected on device: %s',
|
||||
self.host)
|
||||
_LOGGER.debug(
|
||||
"Group name change detected on device: %s", self.host)
|
||||
self._group_name = group_name
|
||||
# the sleep is needed to make sure that the
|
||||
# devices is synced
|
||||
|
@ -398,14 +402,20 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
await self.force_update_sync_status()
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
elif response.status == 595:
|
||||
_LOGGER.info("Status 595 returned, treating as timeout")
|
||||
raise BluesoundPlayer._TimeoutException()
|
||||
else:
|
||||
_LOGGER.error("Error %s on %s. Trying one more time",
|
||||
response.status, url)
|
||||
|
||||
except (asyncio.TimeoutError, ClientError):
|
||||
self._is_online = False
|
||||
self._last_status_update = None
|
||||
self._status = None
|
||||
self.async_schedule_update_ha_state()
|
||||
_LOGGER.info("Client connection error, marking %s as offline",
|
||||
self._name)
|
||||
_LOGGER.info(
|
||||
"Client connection error, marking %s as offline", self._name)
|
||||
raise
|
||||
|
||||
async def async_trigger_sync_on_all(self):
|
||||
|
@ -416,8 +426,8 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
await player.force_update_sync_status()
|
||||
|
||||
@Throttle(SYNC_STATUS_INTERVAL)
|
||||
async def async_update_sync_status(self, on_updated_cb=None,
|
||||
raise_timeout=False):
|
||||
async def async_update_sync_status(
|
||||
self, on_updated_cb=None, raise_timeout=False):
|
||||
"""Update sync status."""
|
||||
await self.force_update_sync_status(
|
||||
on_updated_cb, raise_timeout=False)
|
||||
|
@ -465,7 +475,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
'image': item.get('@image', ''),
|
||||
'is_raw_url': True,
|
||||
'url2': item.get('@url', ''),
|
||||
'url': 'Preset?id=' + item.get('@id', '')
|
||||
'url': 'Preset?id={}'.format(item.get('@id', ''))
|
||||
})
|
||||
|
||||
if 'presets' in resp and 'preset' in resp['presets']:
|
||||
|
@ -503,11 +513,6 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
|
||||
return self._services_items
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No need to poll information."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
|
@ -803,22 +808,22 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
|
||||
async def async_add_slave(self, slave_device):
|
||||
"""Add slave to master."""
|
||||
return self.send_bluesound_command('/AddSlave?slave={}&port={}'
|
||||
.format(slave_device.host,
|
||||
slave_device.port))
|
||||
return await self.send_bluesound_command(
|
||||
'/AddSlave?slave={}&port={}'.format(
|
||||
slave_device.host, slave_device.port))
|
||||
|
||||
async def async_remove_slave(self, slave_device):
|
||||
"""Remove slave to master."""
|
||||
return self.send_bluesound_command('/RemoveSlave?slave={}&port={}'
|
||||
.format(slave_device.host,
|
||||
slave_device.port))
|
||||
return await self.send_bluesound_command(
|
||||
'/RemoveSlave?slave={}&port={}'.format(
|
||||
slave_device.host, slave_device.port))
|
||||
|
||||
async def async_increase_timer(self):
|
||||
"""Increase sleep time on player."""
|
||||
sleep_time = await self.send_bluesound_command('/Sleep')
|
||||
if sleep_time is None:
|
||||
_LOGGER.error('Error while increasing sleep time on player: %s',
|
||||
self.host)
|
||||
_LOGGER.error(
|
||||
"Error while increasing sleep time on player: %s", self.host)
|
||||
return 0
|
||||
|
||||
return int(sleep_time.get('sleep', '0'))
|
||||
|
@ -831,8 +836,9 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
|
||||
async def async_set_shuffle(self, shuffle):
|
||||
"""Enable or disable shuffle mode."""
|
||||
return self.send_bluesound_command('/Shuffle?state={}'
|
||||
.format('1' if shuffle else '0'))
|
||||
value = '1' if shuffle else '0'
|
||||
return await self.send_bluesound_command(
|
||||
'/Shuffle?state={}'.format(value))
|
||||
|
||||
async def async_select_source(self, source):
|
||||
"""Select input source."""
|
||||
|
@ -856,14 +862,14 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
if 'is_raw_url' in selected_source and selected_source['is_raw_url']:
|
||||
url = selected_source['url']
|
||||
|
||||
return self.send_bluesound_command(url)
|
||||
return await self.send_bluesound_command(url)
|
||||
|
||||
async def async_clear_playlist(self):
|
||||
"""Clear players playlist."""
|
||||
if self.is_grouped and not self.is_master:
|
||||
return
|
||||
|
||||
return self.send_bluesound_command('Clear')
|
||||
return await self.send_bluesound_command('Clear')
|
||||
|
||||
async def async_media_next_track(self):
|
||||
"""Send media_next command to media player."""
|
||||
|
@ -877,7 +883,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
action['@name'] == 'skip'):
|
||||
cmd = action['@url']
|
||||
|
||||
return self.send_bluesound_command(cmd)
|
||||
return await self.send_bluesound_command(cmd)
|
||||
|
||||
async def async_media_previous_track(self):
|
||||
"""Send media_previous command to media player."""
|
||||
|
@ -891,35 +897,36 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
action['@name'] == 'back'):
|
||||
cmd = action['@url']
|
||||
|
||||
return self.send_bluesound_command(cmd)
|
||||
return await self.send_bluesound_command(cmd)
|
||||
|
||||
async def async_media_play(self):
|
||||
"""Send media_play command to media player."""
|
||||
if self.is_grouped and not self.is_master:
|
||||
return
|
||||
|
||||
return self.send_bluesound_command('Play')
|
||||
return await self.send_bluesound_command('Play')
|
||||
|
||||
async def async_media_pause(self):
|
||||
"""Send media_pause command to media player."""
|
||||
if self.is_grouped and not self.is_master:
|
||||
return
|
||||
|
||||
return self.send_bluesound_command('Pause')
|
||||
return await self.send_bluesound_command('Pause')
|
||||
|
||||
async def async_media_stop(self):
|
||||
"""Send stop command."""
|
||||
if self.is_grouped and not self.is_master:
|
||||
return
|
||||
|
||||
return self.send_bluesound_command('Pause')
|
||||
return await self.send_bluesound_command('Pause')
|
||||
|
||||
async def async_media_seek(self, position):
|
||||
"""Send media_seek command to media player."""
|
||||
if self.is_grouped and not self.is_master:
|
||||
return
|
||||
|
||||
return self.send_bluesound_command('Play?seek=' + str(float(position)))
|
||||
return await self.send_bluesound_command(
|
||||
'Play?seek={}'.format(float(position)))
|
||||
|
||||
async def async_play_media(self, media_type, media_id, **kwargs):
|
||||
"""
|
||||
|
@ -933,9 +940,9 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
url = 'Play?url={}'.format(media_id)
|
||||
|
||||
if kwargs.get(ATTR_MEDIA_ENQUEUE):
|
||||
return self.send_bluesound_command(url)
|
||||
return await self.send_bluesound_command(url)
|
||||
|
||||
return self.send_bluesound_command(url)
|
||||
return await self.send_bluesound_command(url)
|
||||
|
||||
async def async_volume_up(self):
|
||||
"""Volume up the media player."""
|
||||
|
@ -957,7 +964,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
volume = 0
|
||||
elif volume > 1:
|
||||
volume = 1
|
||||
return self.send_bluesound_command(
|
||||
return await self.send_bluesound_command(
|
||||
'Volume?level=' + str(float(volume) * 100))
|
||||
|
||||
async def async_mute_volume(self, mute):
|
||||
|
@ -966,7 +973,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
|||
volume = self.volume_level
|
||||
if volume > 0:
|
||||
self._lastvol = volume
|
||||
return self.send_bluesound_command('Volume?level=0')
|
||||
return await self.send_bluesound_command('Volume?level=0')
|
||||
else:
|
||||
return self.send_bluesound_command(
|
||||
return await self.send_bluesound_command(
|
||||
'Volume?level=' + str(float(self._lastvol) * 100))
|
||||
|
|
|
@ -288,7 +288,8 @@ class CastDevice(MediaPlayerDevice):
|
|||
self._chromecast = None # type: Optional[pychromecast.Chromecast]
|
||||
self.cast_status = None
|
||||
self.media_status = None
|
||||
self.media_status_received = None
|
||||
self.media_status_position = None
|
||||
self.media_status_position_received = None
|
||||
self._available = False # type: bool
|
||||
self._status_listener = None # type: Optional[CastStatusListener]
|
||||
|
||||
|
@ -361,7 +362,8 @@ class CastDevice(MediaPlayerDevice):
|
|||
self._chromecast = None
|
||||
self.cast_status = None
|
||||
self.media_status = None
|
||||
self.media_status_received = None
|
||||
self.media_status_position = None
|
||||
self.media_status_position_received = None
|
||||
self._status_listener.invalidate()
|
||||
self._status_listener = None
|
||||
|
||||
|
@ -388,8 +390,36 @@ class CastDevice(MediaPlayerDevice):
|
|||
|
||||
def new_media_status(self, media_status):
|
||||
"""Handle updates of the media status."""
|
||||
# Only use media position for playing/paused,
|
||||
# and for normal playback rate
|
||||
if (media_status is None or
|
||||
abs(media_status.playback_rate - 1) > 0.01 or
|
||||
not (media_status.player_is_playing or
|
||||
media_status.player_is_paused)):
|
||||
self.media_status_position = None
|
||||
self.media_status_position_received = None
|
||||
else:
|
||||
# Avoid unnecessary state attribute updates if player_state and
|
||||
# calculated position stay the same
|
||||
now = dt_util.utcnow()
|
||||
do_update = \
|
||||
(self.media_status is None or
|
||||
self.media_status_position is None or
|
||||
self.media_status.player_state != media_status.player_state)
|
||||
if not do_update:
|
||||
if media_status.player_is_playing:
|
||||
elapsed = now - self.media_status_position_received
|
||||
do_update = abs(media_status.current_time -
|
||||
(self.media_status_position +
|
||||
elapsed.total_seconds())) > 1
|
||||
else:
|
||||
do_update = \
|
||||
self.media_status_position != media_status.current_time
|
||||
if do_update:
|
||||
self.media_status_position = media_status.current_time
|
||||
self.media_status_position_received = now
|
||||
|
||||
self.media_status = media_status
|
||||
self.media_status_received = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def new_connection_status(self, connection_status):
|
||||
|
@ -595,13 +625,7 @@ class CastDevice(MediaPlayerDevice):
|
|||
@property
|
||||
def media_position(self):
|
||||
"""Position of current playing media in seconds."""
|
||||
if self.media_status is None or \
|
||||
not (self.media_status.player_is_playing or
|
||||
self.media_status.player_is_paused or
|
||||
self.media_status.player_is_idle):
|
||||
return None
|
||||
|
||||
return self.media_status.current_time
|
||||
return self.media_status_position
|
||||
|
||||
@property
|
||||
def media_position_updated_at(self):
|
||||
|
@ -609,7 +633,7 @@ class CastDevice(MediaPlayerDevice):
|
|||
|
||||
Returns value from homeassistant.util.dt.utcnow().
|
||||
"""
|
||||
return self.media_status_received
|
||||
return self.media_status_position_received
|
||||
|
||||
@property
|
||||
def unique_id(self) -> Optional[str]:
|
||||
|
|
|
@ -8,6 +8,7 @@ import asyncio
|
|||
from collections import OrderedDict
|
||||
from functools import wraps
|
||||
import logging
|
||||
import socket
|
||||
import urllib
|
||||
import re
|
||||
|
||||
|
@ -157,13 +158,29 @@ def _check_deprecated_turn_off(hass, turn_off_action):
|
|||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Kodi platform."""
|
||||
if DATA_KODI not in hass.data:
|
||||
hass.data[DATA_KODI] = []
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
tcp_port = config.get(CONF_TCP_PORT)
|
||||
encryption = config.get(CONF_PROXY_SSL)
|
||||
websocket = config.get(CONF_ENABLE_WEBSOCKET)
|
||||
hass.data[DATA_KODI] = dict()
|
||||
|
||||
# Is this a manual configuration?
|
||||
if discovery_info is None:
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
tcp_port = config.get(CONF_TCP_PORT)
|
||||
encryption = config.get(CONF_PROXY_SSL)
|
||||
websocket = config.get(CONF_ENABLE_WEBSOCKET)
|
||||
else:
|
||||
name = "{} ({})".format(DEFAULT_NAME, discovery_info.get('hostname'))
|
||||
host = discovery_info.get('host')
|
||||
port = discovery_info.get('port')
|
||||
tcp_port = DEFAULT_TCP_PORT
|
||||
encryption = DEFAULT_PROXY_SSL
|
||||
websocket = DEFAULT_ENABLE_WEBSOCKET
|
||||
|
||||
# Only add a device once, so discovered devices do not override manual
|
||||
# config.
|
||||
ip_addr = socket.gethostbyname(host)
|
||||
if ip_addr in hass.data[DATA_KODI]:
|
||||
return
|
||||
|
||||
entity = KodiDevice(
|
||||
hass,
|
||||
|
@ -175,7 +192,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
turn_off_action=config.get(CONF_TURN_OFF_ACTION),
|
||||
timeout=config.get(CONF_TIMEOUT), websocket=websocket)
|
||||
|
||||
hass.data[DATA_KODI].append(entity)
|
||||
hass.data[DATA_KODI][ip_addr] = entity
|
||||
async_add_devices([entity], update_before_add=True)
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -189,10 +206,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
if key != 'entity_id'}
|
||||
entity_ids = service.data.get('entity_id')
|
||||
if entity_ids:
|
||||
target_players = [player for player in hass.data[DATA_KODI]
|
||||
target_players = [player
|
||||
for player in hass.data[DATA_KODI].values()
|
||||
if player.entity_id in entity_ids]
|
||||
else:
|
||||
target_players = hass.data[DATA_KODI]
|
||||
target_players = hass.data[DATA_KODI].values()
|
||||
|
||||
update_tasks = []
|
||||
for player in target_players:
|
||||
|
|
|
@ -8,6 +8,7 @@ import logging
|
|||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_OFF,
|
||||
SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK,
|
||||
|
@ -20,11 +21,11 @@ from homeassistant.helpers.dispatcher import (
|
|||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, STATE_OFF,
|
||||
CONF_TIMEOUT, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY,
|
||||
STATE_UNAVAILABLE
|
||||
STATE_UNAVAILABLE, EVENT_HOMEASSISTANT_STOP
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pymediaroom==0.6']
|
||||
REQUIREMENTS = ['pymediaroom==0.6.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -81,12 +82,21 @@ async def async_setup_platform(hass, config, async_add_devices,
|
|||
if not config[CONF_OPTIMISTIC]:
|
||||
from pymediaroom import install_mediaroom_protocol
|
||||
|
||||
already_installed = hass.data.get(DISCOVERY_MEDIAROOM, False)
|
||||
already_installed = hass.data.get(DISCOVERY_MEDIAROOM, None)
|
||||
if not already_installed:
|
||||
await install_mediaroom_protocol(
|
||||
hass.data[DISCOVERY_MEDIAROOM] = await install_mediaroom_protocol(
|
||||
responses_callback=callback_notify)
|
||||
|
||||
@callback
|
||||
def stop_discovery(event):
|
||||
"""Stop discovery of new mediaroom STB's."""
|
||||
_LOGGER.debug("Stopping internal pymediaroom discovery.")
|
||||
hass.data[DISCOVERY_MEDIAROOM].close()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
stop_discovery)
|
||||
|
||||
_LOGGER.debug("Auto discovery installed")
|
||||
hass.data[DISCOVERY_MEDIAROOM] = True
|
||||
|
||||
|
||||
class MediaroomDevice(MediaPlayerDevice):
|
||||
|
@ -120,7 +130,7 @@ class MediaroomDevice(MediaPlayerDevice):
|
|||
self._channel = None
|
||||
self._optimistic = optimistic
|
||||
self._state = STATE_PLAYING if optimistic else STATE_STANDBY
|
||||
self._name = 'Mediaroom {}'.format(device_id)
|
||||
self._name = 'Mediaroom {}'.format(device_id if device_id else host)
|
||||
self._available = True
|
||||
if device_id:
|
||||
self._unique_id = device_id
|
||||
|
|
|
@ -23,7 +23,7 @@ from homeassistant.const import (
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['python-mpd2==0.5.5']
|
||||
REQUIREMENTS = ['python-mpd2==1.0.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ REQUIREMENTS = ['onkyo-eiscp==1.2.4']
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_SOURCES = 'sources'
|
||||
CONF_ZONE2 = 'zone2'
|
||||
|
||||
DEFAULT_NAME = 'Onkyo Receiver'
|
||||
|
||||
|
@ -40,6 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES):
|
||||
{cv.string: cv.string},
|
||||
vol.Optional(CONF_ZONE2, default=False): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
|
@ -57,6 +59,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
eiscp.eISCP(host), config.get(CONF_SOURCES),
|
||||
name=config.get(CONF_NAME)))
|
||||
KNOWN_HOSTS.append(host)
|
||||
|
||||
# Add Zone2 if configured
|
||||
if config.get(CONF_ZONE2):
|
||||
_LOGGER.debug("Setting up zone 2")
|
||||
hosts.append(OnkyoDeviceZone2(eiscp.eISCP(host),
|
||||
config.get(CONF_SOURCES),
|
||||
name=config.get(CONF_NAME) +
|
||||
" Zone 2"))
|
||||
except OSError:
|
||||
_LOGGER.error("Unable to connect to receiver at %s", host)
|
||||
else:
|
||||
|
@ -98,8 +108,9 @@ class OnkyoDevice(MediaPlayerDevice):
|
|||
return result
|
||||
|
||||
def update(self):
|
||||
"""Get the latest details from the device."""
|
||||
"""Get the latest state from the device."""
|
||||
status = self.command('system-power query')
|
||||
|
||||
if not status:
|
||||
return
|
||||
if status[1] == 'on':
|
||||
|
@ -107,9 +118,11 @@ class OnkyoDevice(MediaPlayerDevice):
|
|||
else:
|
||||
self._pwstate = STATE_OFF
|
||||
return
|
||||
|
||||
volume_raw = self.command('volume query')
|
||||
mute_raw = self.command('audio-muting query')
|
||||
current_source_raw = self.command('input-selector query')
|
||||
|
||||
if not (volume_raw and mute_raw and current_source_raw):
|
||||
return
|
||||
|
||||
|
@ -147,12 +160,12 @@ class OnkyoDevice(MediaPlayerDevice):
|
|||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
"""Boolean if volume is currently muted."""
|
||||
"""Return boolean indicating mute status."""
|
||||
return self._muted
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag media player features that are supported."""
|
||||
"""Return media player features that are supported."""
|
||||
return SUPPORT_ONKYO
|
||||
|
||||
@property
|
||||
|
@ -166,7 +179,7 @@ class OnkyoDevice(MediaPlayerDevice):
|
|||
return self._source_list
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off media player."""
|
||||
"""Turn the media player off."""
|
||||
self.command('system-power standby')
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
|
@ -189,3 +202,68 @@ class OnkyoDevice(MediaPlayerDevice):
|
|||
if source in self._source_list:
|
||||
source = self._reverse_mapping[source]
|
||||
self.command('input-selector {}'.format(source))
|
||||
|
||||
|
||||
class OnkyoDeviceZone2(OnkyoDevice):
|
||||
"""Representation of an Onkyo device's zone 2."""
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state from the device."""
|
||||
status = self.command('zone2.power=query')
|
||||
|
||||
if not status:
|
||||
return
|
||||
if status[1] == 'on':
|
||||
self._pwstate = STATE_ON
|
||||
else:
|
||||
self._pwstate = STATE_OFF
|
||||
return
|
||||
|
||||
volume_raw = self.command('zone2.volume=query')
|
||||
mute_raw = self.command('zone2.muting=query')
|
||||
current_source_raw = self.command('zone2.selector=query')
|
||||
|
||||
if not (volume_raw and mute_raw and current_source_raw):
|
||||
return
|
||||
|
||||
# eiscp can return string or tuple. Make everything tuples.
|
||||
if isinstance(current_source_raw[1], str):
|
||||
current_source_tuples = \
|
||||
(current_source_raw[0], (current_source_raw[1],))
|
||||
else:
|
||||
current_source_tuples = current_source_raw
|
||||
|
||||
for source in current_source_tuples[1]:
|
||||
if source in self._source_mapping:
|
||||
self._current_source = self._source_mapping[source]
|
||||
break
|
||||
else:
|
||||
self._current_source = '_'.join(
|
||||
[i for i in current_source_tuples[1]])
|
||||
self._muted = bool(mute_raw[1] == 'on')
|
||||
self._volume = volume_raw[1] / 80.0
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the media player off."""
|
||||
self.command('zone2.power=standby')
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, input is range 0..1. Onkyo ranges from 1-80."""
|
||||
self.command('zone2.volume={}'.format(int(volume*80)))
|
||||
|
||||
def mute_volume(self, mute):
|
||||
"""Mute (true) or unmute (false) media player."""
|
||||
if mute:
|
||||
self.command('zone2.muting=on')
|
||||
else:
|
||||
self.command('zone2.muting=off')
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
self.command('zone2.power=on')
|
||||
|
||||
def select_source(self, source):
|
||||
"""Set the input source."""
|
||||
if source in self._source_list:
|
||||
source = self._reverse_mapping[source]
|
||||
self.command('zone2.selector={}'.format(source))
|
||||
|
|
|
@ -402,3 +402,13 @@ songpal_set_sound_setting:
|
|||
value:
|
||||
description: Value to set.
|
||||
example: 'on'
|
||||
|
||||
blackbird_set_all_zones:
|
||||
description: Set all Blackbird zones to a single source.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of any blackbird zone.
|
||||
example: 'media_player.zone_1'
|
||||
source:
|
||||
description: Name of source to switch to.
|
||||
example: 'Source 1'
|
||||
|
|
|
@ -266,6 +266,8 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
|||
if response is False:
|
||||
return
|
||||
|
||||
last_media_position = self.media_position
|
||||
|
||||
self._status = {}
|
||||
|
||||
try:
|
||||
|
@ -278,7 +280,11 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
|||
pass
|
||||
|
||||
self._status.update(response)
|
||||
self._last_update = utcnow()
|
||||
|
||||
if self.media_position != last_media_position:
|
||||
_LOGGER.debug('Media position updated for %s: %s',
|
||||
self, self.media_position)
|
||||
self._last_update = utcnow()
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
|
|
|
@ -344,6 +344,42 @@ class LgWebOSDevice(MediaPlayerDevice):
|
|||
self._current_source = source_dict['label']
|
||||
self._client.set_input(source_dict['id'])
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Play a piece of media."""
|
||||
_LOGGER.debug(
|
||||
"Call play media type <%s>, Id <%s>", media_type, media_id)
|
||||
|
||||
if media_type == MEDIA_TYPE_CHANNEL:
|
||||
_LOGGER.debug("Searching channel...")
|
||||
partial_match_channel_id = None
|
||||
|
||||
for channel in self._client.get_channels():
|
||||
_LOGGER.debug(
|
||||
"Checking channel number <%s>, name <%s>, id <%s>...",
|
||||
channel['channelNumber'],
|
||||
channel['channelName'],
|
||||
channel['channelId'])
|
||||
if media_id == channel['channelNumber']:
|
||||
_LOGGER.debug(
|
||||
"Perfect match on channel number: switching!")
|
||||
self._client.set_channel(channel['channelId'])
|
||||
return
|
||||
elif media_id.lower() == channel['channelName'].lower():
|
||||
_LOGGER.debug(
|
||||
"Perfect match on channel name: switching!")
|
||||
self._client.set_channel(channel['channelId'])
|
||||
return
|
||||
elif media_id.lower() in channel['channelName'].lower():
|
||||
_LOGGER.debug(
|
||||
"Partial match on channel name: saving it...")
|
||||
partial_match_channel_id = channel['channelId']
|
||||
|
||||
if partial_match_channel_id is not None:
|
||||
_LOGGER.debug(
|
||||
"Using partial match on channel name: switching!")
|
||||
self._client.set_channel(partial_match_channel_id)
|
||||
return
|
||||
|
||||
def media_play(self):
|
||||
"""Send play command."""
|
||||
self._playing = True
|
||||
|
|
|
@ -37,7 +37,8 @@ PLATFORM_SCHEMA = vol.Schema(
|
|||
vol.All(PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_RECIPIENT): cv.string,
|
||||
vol.Required(CONF_RECIPIENT, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_SENDER): cv.string,
|
||||
}), validate_sender))
|
||||
|
||||
|
@ -59,21 +60,19 @@ class ClicksendNotificationService(BaseNotificationService):
|
|||
"""Initialize the service."""
|
||||
self.username = config.get(CONF_USERNAME)
|
||||
self.api_key = config.get(CONF_API_KEY)
|
||||
self.recipient = config.get(CONF_RECIPIENT)
|
||||
self.recipients = config.get(CONF_RECIPIENT)
|
||||
self.sender = config.get(CONF_SENDER, CONF_RECIPIENT)
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a user."""
|
||||
data = ({
|
||||
'messages': [
|
||||
{
|
||||
'source': 'hass.notify',
|
||||
'from': self.sender,
|
||||
'to': self.recipient,
|
||||
'body': message,
|
||||
}
|
||||
]
|
||||
})
|
||||
data = {"messages": []}
|
||||
for recipient in self.recipients:
|
||||
data["messages"].append({
|
||||
'source': 'hass.notify',
|
||||
'from': self.sender,
|
||||
'to': recipient,
|
||||
'body': message,
|
||||
})
|
||||
|
||||
api_url = "{}/sms/send".format(BASE_API_URL)
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ Facebook platform for notify component.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.facebook/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
|
@ -19,6 +20,8 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
CONF_PAGE_ACCESS_TOKEN = 'page_access_token'
|
||||
BASE_URL = 'https://graph.facebook.com/v2.6/me/messages'
|
||||
CREATE_BROADCAST_URL = 'https://graph.facebook.com/v2.11/me/message_creatives'
|
||||
SEND_BROADCAST_URL = 'https://graph.facebook.com/v2.11/me/broadcast_messages'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PAGE_ACCESS_TOKEN): cv.string,
|
||||
|
@ -55,27 +58,60 @@ class FacebookNotificationService(BaseNotificationService):
|
|||
_LOGGER.error("At least 1 target is required")
|
||||
return
|
||||
|
||||
for target in targets:
|
||||
# If the target starts with a "+", we suppose it's a phone number,
|
||||
# otherwise it's a user id.
|
||||
if target.startswith('+'):
|
||||
recipient = {"phone_number": target}
|
||||
else:
|
||||
recipient = {"id": target}
|
||||
# broadcast message
|
||||
if targets[0].lower() == 'broadcast':
|
||||
broadcast_create_body = {"messages": [body_message]}
|
||||
_LOGGER.debug("Broadcast body %s : ", broadcast_create_body)
|
||||
|
||||
body = {
|
||||
"recipient": recipient,
|
||||
"message": body_message
|
||||
resp = requests.post(CREATE_BROADCAST_URL,
|
||||
data=json.dumps(broadcast_create_body),
|
||||
params=payload,
|
||||
headers={CONTENT_TYPE: CONTENT_TYPE_JSON},
|
||||
timeout=10)
|
||||
_LOGGER.debug("FB Messager broadcast id %s : ", resp.json())
|
||||
|
||||
# at this point we get broadcast id
|
||||
broadcast_body = {
|
||||
"message_creative_id": resp.json().get('message_creative_id'),
|
||||
"notification_type": "REGULAR",
|
||||
}
|
||||
import json
|
||||
resp = requests.post(BASE_URL, data=json.dumps(body),
|
||||
|
||||
resp = requests.post(SEND_BROADCAST_URL,
|
||||
data=json.dumps(broadcast_body),
|
||||
params=payload,
|
||||
headers={CONTENT_TYPE: CONTENT_TYPE_JSON},
|
||||
timeout=10)
|
||||
if resp.status_code != 200:
|
||||
obj = resp.json()
|
||||
error_message = obj['error']['message']
|
||||
error_code = obj['error']['code']
|
||||
_LOGGER.error(
|
||||
"Error %s : %s (Code %s)", resp.status_code, error_message,
|
||||
error_code)
|
||||
log_error(resp)
|
||||
|
||||
# non-broadcast message
|
||||
else:
|
||||
for target in targets:
|
||||
# If the target starts with a "+", it's a phone number,
|
||||
# otherwise it's a user id.
|
||||
if target.startswith('+'):
|
||||
recipient = {"phone_number": target}
|
||||
else:
|
||||
recipient = {"id": target}
|
||||
|
||||
body = {
|
||||
"recipient": recipient,
|
||||
"message": body_message
|
||||
}
|
||||
resp = requests.post(BASE_URL, data=json.dumps(body),
|
||||
params=payload,
|
||||
headers={CONTENT_TYPE: CONTENT_TYPE_JSON},
|
||||
timeout=10)
|
||||
if resp.status_code != 200:
|
||||
log_error(resp)
|
||||
|
||||
|
||||
def log_error(response):
|
||||
"""Log error message."""
|
||||
obj = response.json()
|
||||
error_message = obj['error']['message']
|
||||
error_code = obj['error']['code']
|
||||
|
||||
_LOGGER.error(
|
||||
"Error %s : %s (Code %s)", response.status_code, error_message,
|
||||
error_code)
|
||||
|
|
|
@ -76,8 +76,6 @@ def send_message(sender, password, recipient, use_tls,
|
|||
"""Initialize the Jabber Bot."""
|
||||
super(SendNotificationBot, self).__init__(sender, password)
|
||||
|
||||
logging.basicConfig(level=logging.ERROR)
|
||||
|
||||
self.use_tls = use_tls
|
||||
self.use_ipv6 = False
|
||||
self.add_event_handler('failed_auth', self.check_credentials)
|
||||
|
|
|
@ -185,6 +185,9 @@ class Metrics(object):
|
|||
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
metric = state.entity_id.split(".")[1]
|
||||
|
||||
if '_' not in str(metric):
|
||||
metric = state.entity_id.replace('.', '_')
|
||||
|
||||
try:
|
||||
int(metric.split("_")[-1])
|
||||
metric = "_".join(metric.split("_")[:-1])
|
||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.helpers.entity import Entity
|
|||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyqwikswitch==0.6']
|
||||
REQUIREMENTS = ['pyqwikswitch==0.71']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -34,17 +34,54 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
vol.Coerce(str),
|
||||
vol.Optional(CONF_DIMMER_ADJUST, default=1): CV_DIM_VALUE,
|
||||
vol.Optional(CONF_BUTTON_EVENTS, default=[]): cv.ensure_list_csv,
|
||||
vol.Optional(CONF_SENSORS, default={}): vol.Schema({cv.slug: str}),
|
||||
vol.Optional(CONF_SENSORS, default=[]): vol.All(
|
||||
cv.ensure_list, [vol.Schema({
|
||||
vol.Required('id'): str,
|
||||
vol.Optional('channel', default=1): int,
|
||||
vol.Required('name'): str,
|
||||
vol.Required('type'): str,
|
||||
})]),
|
||||
vol.Optional(CONF_SWITCHES, default=[]): vol.All(
|
||||
cv.ensure_list, [str])
|
||||
})}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
class QSToggleEntity(Entity):
|
||||
"""Representation of a Qwikswitch Entity.
|
||||
class QSEntity(Entity):
|
||||
"""Qwikswitch Entity base."""
|
||||
|
||||
Implement base QS methods. Modeled around HA ToggleEntity[1] & should only
|
||||
be used in a class that extends both QSToggleEntity *and* ToggleEntity.
|
||||
def __init__(self, qsid, name):
|
||||
"""Initialize the QSEntity."""
|
||||
self._name = name
|
||||
self.qsid = qsid
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def poll(self):
|
||||
"""QS sensors gets packets in update_packet."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique identifier for this sensor."""
|
||||
return "qs{}".format(self.qsid)
|
||||
|
||||
@callback
|
||||
def update_packet(self, packet):
|
||||
"""Receive update packet from QSUSB. Match dispather_send signature."""
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Listen for updates from QSUSb via dispatcher."""
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
self.qsid, self.update_packet)
|
||||
|
||||
|
||||
class QSToggleEntity(QSEntity):
|
||||
"""Representation of a Qwikswitch Toggle Entity.
|
||||
|
||||
Implemented:
|
||||
- QSLight extends QSToggleEntity and Light[2] (ToggleEntity[1])
|
||||
|
@ -57,52 +94,28 @@ class QSToggleEntity(Entity):
|
|||
|
||||
def __init__(self, qsid, qsusb):
|
||||
"""Initialize the ToggleEntity."""
|
||||
from pyqwikswitch import (QS_NAME, QSDATA, QS_TYPE, QSType)
|
||||
self.qsid = qsid
|
||||
self._qsusb = qsusb.devices
|
||||
dev = qsusb.devices[qsid]
|
||||
self._dim = dev[QS_TYPE] == QSType.dimmer
|
||||
self._name = dev[QSDATA][QS_NAME]
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the light."""
|
||||
return self._name
|
||||
self.device = qsusb.devices[qsid]
|
||||
super().__init__(qsid, self.device.name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Check if device is on (non-zero)."""
|
||||
return self._qsusb[self.qsid, 1] > 0
|
||||
return self.device.value > 0
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
new = kwargs.get(ATTR_BRIGHTNESS, 255)
|
||||
self._qsusb.set_value(self.qsid, new)
|
||||
self.hass.data[DOMAIN].devices.set_value(self.qsid, new)
|
||||
|
||||
async def async_turn_off(self, **_):
|
||||
"""Turn the device off."""
|
||||
self._qsusb.set_value(self.qsid, 0)
|
||||
|
||||
def _update(self, _packet=None):
|
||||
"""Schedule an update - match dispather_send signature."""
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Listen for updates from QSUSb via dispatcher."""
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
self.qsid, self._update)
|
||||
self.hass.data[DOMAIN].devices.set_value(self.qsid, 0)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Qwiskswitch component setup."""
|
||||
from pyqwikswitch.async_ import QSUsb
|
||||
from pyqwikswitch import (
|
||||
CMD_BUTTONS, QS_CMD, QS_ID, QS_TYPE, QSType)
|
||||
from pyqwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType
|
||||
|
||||
# Add cmd's to in /&listen packets will fire events
|
||||
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL]
|
||||
|
@ -112,8 +125,8 @@ async def async_setup(hass, config):
|
|||
|
||||
url = config[DOMAIN][CONF_URL]
|
||||
dimmer_adjust = config[DOMAIN][CONF_DIMMER_ADJUST]
|
||||
sensors = config[DOMAIN]['sensors']
|
||||
switches = config[DOMAIN]['switches']
|
||||
sensors = config[DOMAIN][CONF_SENSORS]
|
||||
switches = config[DOMAIN][CONF_SWITCHES]
|
||||
|
||||
def callback_value_changed(_qsd, qsid, _val):
|
||||
"""Update entity values based on device change."""
|
||||
|
@ -131,17 +144,17 @@ async def async_setup(hass, config):
|
|||
hass.data[DOMAIN] = qsusb
|
||||
|
||||
_new = {'switch': [], 'light': [], 'sensor': sensors}
|
||||
for _id, item in qsusb.devices:
|
||||
if _id in switches:
|
||||
if item[QS_TYPE] != QSType.relay:
|
||||
for qsid, dev in qsusb.devices.items():
|
||||
if qsid in switches:
|
||||
if dev.qstype != QSType.relay:
|
||||
_LOGGER.warning(
|
||||
"You specified a switch that is not a relay %s", _id)
|
||||
"You specified a switch that is not a relay %s", qsid)
|
||||
continue
|
||||
_new['switch'].append(_id)
|
||||
elif item[QS_TYPE] in [QSType.relay, QSType.dimmer]:
|
||||
_new['light'].append(_id)
|
||||
_new['switch'].append(qsid)
|
||||
elif dev.qstype in (QSType.relay, QSType.dimmer):
|
||||
_new['light'].append(qsid)
|
||||
else:
|
||||
_LOGGER.warning("Ignored unknown QSUSB device: %s", item)
|
||||
_LOGGER.warning("Ignored unknown QSUSB device: %s", dev)
|
||||
continue
|
||||
|
||||
# Load platforms
|
||||
|
@ -149,24 +162,21 @@ async def async_setup(hass, config):
|
|||
if comp_conf:
|
||||
load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config)
|
||||
|
||||
def callback_qs_listen(item):
|
||||
def callback_qs_listen(qspacket):
|
||||
"""Typically a button press or update signal."""
|
||||
# If button pressed, fire a hass event
|
||||
if QS_ID in item:
|
||||
if item.get(QS_CMD, '') in cmd_buttons:
|
||||
if QS_ID in qspacket:
|
||||
if qspacket.get(QS_CMD, '') in cmd_buttons:
|
||||
hass.bus.async_fire(
|
||||
'qwikswitch.button.{}'.format(item[QS_ID]), item)
|
||||
'qwikswitch.button.{}'.format(qspacket[QS_ID]), qspacket)
|
||||
return
|
||||
|
||||
# Private method due to bad __iter__ design in qsusb
|
||||
# qsusb.devices returns a list of tuples
|
||||
if item[QS_ID] not in \
|
||||
qsusb.devices._data: # pylint: disable=protected-access
|
||||
if qspacket[QS_ID] not in qsusb.devices:
|
||||
# Not a standard device in, component can handle packet
|
||||
# i.e. sensors
|
||||
_LOGGER.debug("Dispatch %s ((%s))", item[QS_ID], item)
|
||||
_LOGGER.debug("Dispatch %s ((%s))", qspacket[QS_ID], qspacket)
|
||||
hass.helpers.dispatcher.async_dispatcher_send(
|
||||
item[QS_ID], item)
|
||||
qspacket[QS_ID], qspacket)
|
||||
|
||||
# Update all ha_objects
|
||||
hass.async_add_job(qsusb.update_from_devices)
|
||||
|
|
|
@ -35,7 +35,7 @@ from . import migration, purge
|
|||
from .const import DATA_INSTANCE
|
||||
from .util import session_scope
|
||||
|
||||
REQUIREMENTS = ['sqlalchemy==1.2.5']
|
||||
REQUIREMENTS = ['sqlalchemy==1.2.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -47,9 +47,8 @@ ATTR_KEEP_DAYS = 'keep_days'
|
|||
ATTR_REPACK = 'repack'
|
||||
|
||||
SERVICE_PURGE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_KEEP_DAYS):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
vol.Optional(ATTR_REPACK, default=False): cv.boolean
|
||||
vol.Optional(ATTR_KEEP_DAYS): vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
vol.Optional(ATTR_REPACK, default=False): cv.boolean,
|
||||
})
|
||||
|
||||
DEFAULT_URL = 'sqlite:///{hass_config_path}'
|
||||
|
|
|
@ -8,6 +8,8 @@ https://home-assistant.io/components/sensor/
|
|||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
|
||||
|
@ -18,6 +20,13 @@ DOMAIN = 'sensor'
|
|||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
DEVICE_CLASSES = [
|
||||
'battery', # % of battery that is left
|
||||
'humidity', # % of humidity in the air
|
||||
'temperature', # temperature (C/F)
|
||||
]
|
||||
|
||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['alpha_vantage==1.9.0']
|
||||
REQUIREMENTS = ['alpha_vantage==2.0.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ class BMWConnectedDriveSensor(Entity):
|
|||
self._state = None
|
||||
self._unit_of_measurement = None
|
||||
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
|
||||
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
|
||||
self._sensor_name = sensor_name
|
||||
self._icon = icon
|
||||
|
||||
|
@ -60,6 +61,11 @@ class BMWConnectedDriveSensor(Entity):
|
|||
"""Data update is triggered from BMWConnectedDriveEntity."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of the sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
|
@ -86,7 +92,7 @@ class BMWConnectedDriveSensor(Entity):
|
|||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the binary sensor."""
|
||||
"""Return the state attributes of the sensor."""
|
||||
return {
|
||||
'car': self._vehicle.name
|
||||
}
|
||||
|
|
|
@ -56,9 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
name = config.get(CONF_NAME)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
update_interval = config.get(CONF_UPDATE_INTERVAL)
|
||||
|
||||
broadlink_data = BroadlinkData(update_interval, host, mac_addr, timeout)
|
||||
|
||||
dev = []
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]:
|
||||
dev.append(BroadlinkSensor(name, broadlink_data, variable))
|
||||
|
@ -104,10 +102,11 @@ class BroadlinkData(object):
|
|||
|
||||
def __init__(self, interval, ip_addr, mac_addr, timeout):
|
||||
"""Initialize the data object."""
|
||||
import broadlink
|
||||
self.data = None
|
||||
self._device = broadlink.a1((ip_addr, 80), mac_addr, None)
|
||||
self._device.timeout = timeout
|
||||
self.ip_addr = ip_addr
|
||||
self.mac_addr = mac_addr
|
||||
self.timeout = timeout
|
||||
self._connect()
|
||||
self._schema = vol.Schema({
|
||||
vol.Optional('temperature'): vol.Range(min=-50, max=150),
|
||||
vol.Optional('humidity'): vol.Range(min=0, max=100),
|
||||
|
@ -119,6 +118,11 @@ class BroadlinkData(object):
|
|||
if not self._auth():
|
||||
_LOGGER.warning("Failed to connect to device")
|
||||
|
||||
def _connect(self):
|
||||
import broadlink
|
||||
self._device = broadlink.a1((self.ip_addr, 80), self.mac_addr, None)
|
||||
self._device.timeout = self.timeout
|
||||
|
||||
def _update(self, retry=3):
|
||||
try:
|
||||
data = self._device.check_sensors_raw()
|
||||
|
@ -140,5 +144,6 @@ class BroadlinkData(object):
|
|||
except socket.timeout:
|
||||
auth = False
|
||||
if not auth and retry > 0:
|
||||
self._connect()
|
||||
return self._auth(retry-1)
|
||||
return auth
|
||||
|
|
|
@ -16,6 +16,7 @@ from homeassistant.util import slugify
|
|||
DEPENDENCIES = ['deconz']
|
||||
|
||||
ATTR_CURRENT = 'current'
|
||||
ATTR_DAYLIGHT = 'daylight'
|
||||
ATTR_EVENT_ID = 'event_id'
|
||||
|
||||
|
||||
|
@ -113,6 +114,8 @@ class DeconzSensor(Entity):
|
|||
if self.unit_of_measurement == 'Watts':
|
||||
attr[ATTR_CURRENT] = self._sensor.current
|
||||
attr[ATTR_VOLTAGE] = self._sensor.voltage
|
||||
if self._sensor.sensor_class == 'daylight':
|
||||
attr[ATTR_DAYLIGHT] = self._sensor.daylight
|
||||
return attr
|
||||
|
||||
|
||||
|
|
|
@ -19,7 +19,8 @@ from homeassistant.const import (
|
|||
CONF_NAME, CONF_MONITORED_VARIABLES)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['pyebox==0.1.0']
|
||||
# pylint: disable=import-error
|
||||
REQUIREMENTS = [] # ['pyebox==0.1.0'] - disabled because it breaks pip10
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -52,6 +52,13 @@ class EcobeeSensor(Entity):
|
|||
"""Return the name of the Ecobee sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of the sensor."""
|
||||
if self.type in ('temperature', 'humidity'):
|
||||
return self.type
|
||||
return None
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue