Merge branch 'dev' into rc

This commit is contained in:
Paulus Schoutsen 2018-04-20 10:58:25 -04:00
commit a566804f7f
194 changed files with 7617 additions and 2176 deletions

View file

@ -94,6 +94,12 @@ omit =
homeassistant/components/envisalink.py homeassistant/components/envisalink.py
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
homeassistant/components/*/gc100.py homeassistant/components/*/gc100.py
@ -106,6 +112,9 @@ omit =
homeassistant/components/hive.py homeassistant/components/hive.py
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/__init__.py
homeassistant/components/*/homematic.py homeassistant/components/*/homematic.py
@ -190,8 +199,8 @@ omit =
homeassistant/components/pilight.py homeassistant/components/pilight.py
homeassistant/components/*/pilight.py homeassistant/components/*/pilight.py
homeassistant/components/qwikswitch.py homeassistant/components/switch/qwikswitch.py
homeassistant/components/*/qwikswitch.py homeassistant/components/light/qwikswitch.py
homeassistant/components/rachio.py homeassistant/components/rachio.py
homeassistant/components/*/rachio.py homeassistant/components/*/rachio.py
@ -639,7 +648,9 @@ omit =
homeassistant/components/sensor/sensehat.py homeassistant/components/sensor/sensehat.py
homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/serial.py homeassistant/components/sensor/serial.py
homeassistant/components/sensor/sht31.py
homeassistant/components/sensor/shodan.py homeassistant/components/sensor/shodan.py
homeassistant/components/sensor/sigfox.py
homeassistant/components/sensor/simulated.py homeassistant/components/sensor/simulated.py
homeassistant/components/sensor/skybeacon.py homeassistant/components/sensor/skybeacon.py
homeassistant/components/sensor/sma.py homeassistant/components/sensor/sma.py
@ -669,6 +680,7 @@ omit =
homeassistant/components/sensor/uber.py homeassistant/components/sensor/uber.py
homeassistant/components/sensor/upnp.py homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/ups.py homeassistant/components/sensor/ups.py
homeassistant/components/sensor/uscis.py
homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/viaggiatreno.py homeassistant/components/sensor/viaggiatreno.py
homeassistant/components/sensor/waqi.py homeassistant/components/sensor/waqi.py

View file

@ -31,7 +31,7 @@ script: travis_wait 30 tox --develop
services: services:
- docker - docker
before_deploy: before_deploy:
- docker pull lokalise/lokalise-cli@sha256:79b3108211ed1fcc9f7b09a011bfc53c240fc2f3b7fa7f0c8390f593271b4cd7 - docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21
deploy: deploy:
skip_cleanup: true skip_cleanup: true
provider: script provider: script

View file

@ -63,6 +63,7 @@ homeassistant/components/media_player/xiaomi_tv.py @fattdev
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
homeassistant/components/plant.py @ChristianKuehnel homeassistant/components/plant.py @ChristianKuehnel
homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/filter.py @dgomes
homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel 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/sql.py @dgomes
homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/tibber.py @danielhiversen
homeassistant/components/sensor/upnp.py @dgomes
homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/sensor/waqi.py @andrey-git
homeassistant/components/switch/rainmachine.py @bachya homeassistant/components/switch/rainmachine.py @bachya
homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/switch/tplink.py @rytilahti

View file

@ -126,6 +126,10 @@ def get_arguments() -> argparse.Namespace:
default=None, default=None,
help='Log file to write to. If not set, CONFIG/home-assistant.log ' help='Log file to write to. If not set, CONFIG/home-assistant.log '
'is used') 'is used')
parser.add_argument(
'--log-no-color',
action='store_true',
help="Disable color logs")
parser.add_argument( parser.add_argument(
'--runner', '--runner',
action='store_true', action='store_true',
@ -259,13 +263,14 @@ def setup_and_run_hass(config_dir: str,
hass = bootstrap.from_config_dict( hass = bootstrap.from_config_dict(
config, config_dir=config_dir, verbose=args.verbose, config, config_dir=config_dir, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days, 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: else:
config_file = ensure_config_file(config_dir) config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir) print('Config directory:', config_dir)
hass = bootstrap.from_config_file( hass = bootstrap.from_config_file(
config_file, verbose=args.verbose, skip_pip=args.skip_pip, 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: if hass is None:
return None return None

View file

@ -42,7 +42,8 @@ def from_config_dict(config: Dict[str, Any],
verbose: bool = False, verbose: bool = False,
skip_pip: bool = False, skip_pip: bool = False,
log_rotate_days: Any = None, log_rotate_days: Any = None,
log_file: Any = None) \ log_file: Any = None,
log_no_color: bool = False) \
-> Optional[core.HomeAssistant]: -> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary. """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( hass = hass.loop.run_until_complete(
async_from_config_dict( async_from_config_dict(
config, hass, config_dir, enable_log, verbose, skip_pip, config, hass, config_dir, enable_log, verbose, skip_pip,
log_rotate_days, log_file) log_rotate_days, log_file, log_no_color)
) )
return hass return hass
@ -74,7 +75,8 @@ def async_from_config_dict(config: Dict[str, Any],
verbose: bool = False, verbose: bool = False,
skip_pip: bool = False, skip_pip: bool = False,
log_rotate_days: Any = None, log_rotate_days: Any = None,
log_file: Any = None) \ log_file: Any = None,
log_no_color: bool = False) \
-> Optional[core.HomeAssistant]: -> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary. """Try to configure Home Assistant from a configuration dictionary.
@ -84,7 +86,8 @@ def async_from_config_dict(config: Dict[str, Any],
start = time() start = time()
if enable_log: 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, {}) core_config = config.get(core.DOMAIN, {})
@ -164,7 +167,8 @@ def from_config_file(config_path: str,
verbose: bool = False, verbose: bool = False,
skip_pip: bool = True, skip_pip: bool = True,
log_rotate_days: Any = None, 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. """Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given, Will add functionality to 'hass' parameter if given,
@ -176,7 +180,8 @@ def from_config_file(config_path: str,
# run task # run task
hass = hass.loop.run_until_complete( hass = hass.loop.run_until_complete(
async_from_config_file( 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 return hass
@ -188,7 +193,8 @@ def async_from_config_file(config_path: str,
verbose: bool = False, verbose: bool = False,
skip_pip: bool = True, skip_pip: bool = True,
log_rotate_days: Any = None, 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. """Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter. Will add functionality to 'hass' parameter.
@ -199,7 +205,8 @@ def async_from_config_file(config_path: str,
hass.config.config_dir = config_dir hass.config.config_dir = config_dir
yield from async_mount_local_lib_path(config_dir, hass.loop) 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: try:
config_dict = yield from hass.async_add_job( config_dict = yield from hass.async_add_job(
@ -216,25 +223,27 @@ def async_from_config_file(config_path: str,
@core.callback @core.callback
def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False, def async_enable_logging(hass: core.HomeAssistant,
log_rotate_days=None, log_file=None) -> None: verbose: bool = False,
log_rotate_days=None,
log_file=None,
log_no_color: bool = False) -> None:
"""Set up the logging. """Set up the logging.
This method must be run in the event loop. This method must be run in the event loop.
""" """
logging.basicConfig(level=logging.INFO)
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) " fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s") "[%(name)s] %(message)s")
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
datefmt = '%Y-%m-%d %H:%M:%S' datefmt = '%Y-%m-%d %H:%M:%S'
# Suppress overly verbose logs from libraries that aren't helpful if not log_no_color:
logging.getLogger('requests').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('aiohttp.access').setLevel(logging.WARNING)
try: try:
from colorlog import ColoredFormatter 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( logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
colorfmt, colorfmt,
datefmt=datefmt, datefmt=datefmt,
@ -250,6 +259,15 @@ def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False,
except ImportError: except ImportError:
pass 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)
# Log errors to a file if we have write access to file or config dir # Log errors to a file if we have write access to file or config dir
if log_file is None: if log_file is None:
err_log_path = hass.config.path(ERROR_LOG_FILENAME) err_log_path = hass.config.path(ERROR_LOG_FILENAME)

View file

@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['abodepy==0.12.3'] REQUIREMENTS = ['abodepy==0.13.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -27,6 +27,7 @@ CONF_ATTRIBUTION = "Data provided by goabode.com"
CONF_POLLING = 'polling' CONF_POLLING = 'polling'
DOMAIN = 'abode' DOMAIN = 'abode'
DEFAULT_CACHEDB = './abodepy_cache.pickle'
NOTIFICATION_ID = 'abode_notification' NOTIFICATION_ID = 'abode_notification'
NOTIFICATION_TITLE = 'Abode Security Setup' NOTIFICATION_TITLE = 'Abode Security Setup'
@ -87,12 +88,13 @@ ABODE_PLATFORMS = [
class AbodeSystem(object): class AbodeSystem(object):
"""Abode System class.""" """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.""" """Initialize the system."""
import abodepy import abodepy
self.abode = abodepy.Abode( self.abode = abodepy.Abode(
username, password, auto_login=True, get_devices=True, username, password, auto_login=True, get_devices=True,
get_automations=True) get_automations=True, cache_path=cache)
self.name = name self.name = name
self.polling = polling self.polling = polling
self.exclude = exclude self.exclude = exclude
@ -129,8 +131,9 @@ def setup(hass, config):
lights = conf.get(CONF_LIGHTS) lights = conf.get(CONF_LIGHTS)
try: try:
cache = hass.config.path(DEFAULT_CACHEDB)
hass.data[DOMAIN] = AbodeSystem( hass.data[DOMAIN] = AbodeSystem(
username, password, name, polling, exclude, lights) username, password, cache, name, polling, exclude, lights)
except (AbodeException, ConnectTimeout, HTTPError) as ex: except (AbodeException, ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Abode: %s", str(ex)) _LOGGER.error("Unable to connect to Abode: %s", str(ex))

View file

@ -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): async def async_api_set_thermostat_mode(hass, config, request, entity):
"""Process a set thermostat mode request.""" """Process a set thermostat mode request."""
mode = request[API_PAYLOAD]['thermostatMode'] mode = request[API_PAYLOAD]['thermostatMode']
mode = mode if isinstance(mode, str) else mode['value']
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST) operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
# Work around a pylint false positive due to # Work around a pylint false positive due to

View file

@ -46,6 +46,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
self._vehicle = vehicle self._vehicle = vehicle
self._attribute = attribute self._attribute = attribute
self._name = '{} {}'.format(self._vehicle.name, self._attribute) self._name = '{} {}'.format(self._vehicle.name, self._attribute)
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
self._sensor_name = sensor_name self._sensor_name = sensor_name
self._device_class = device_class self._device_class = device_class
self._state = None self._state = None
@ -55,6 +56,11 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
"""Data update is triggered from BMWConnectedDriveEntity.""" """Data update is triggered from BMWConnectedDriveEntity."""
return False return False
@property
def unique_id(self):
"""Return the unique ID of the binary sensor."""
return self._unique_id
@property @property
def name(self): def name(self):
"""Return the name of the binary sensor.""" """Return the name of the binary sensor."""

View file

@ -32,6 +32,7 @@ class HiveBinarySensorEntity(BinarySensorDevice):
self.device_type = hivedevice["HA_DeviceType"] self.device_type = hivedevice["HA_DeviceType"]
self.node_device_type = hivedevice["Hive_DeviceType"] self.node_device_type = hivedevice["Hive_DeviceType"]
self.session = hivesession self.session = hivesession
self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type, self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id) self.node_id)
@ -52,6 +53,11 @@ class HiveBinarySensorEntity(BinarySensorDevice):
"""Return the name of the binary sensor.""" """Return the name of the binary sensor."""
return self.node_name return self.node_name
@property
def device_state_attributes(self):
"""Show Device Attributes."""
return self.attributes
@property @property
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
@ -61,3 +67,5 @@ class HiveBinarySensorEntity(BinarySensorDevice):
def update(self): def update(self):
"""Update all Node data from Hive.""" """Update all Node data from Hive."""
self.session.core.update_data(self.node_id) self.session.core.update_data(self.node_id)
self.attributes = self.session.attributes.state_attributes(
self.node_id)

View file

@ -7,7 +7,7 @@ https://home-assistant.io/components/maxcube/
import logging import logging
from homeassistant.components.binary_sensor import BinarySensorDevice 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 from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -15,16 +15,17 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Iterate through all MAX! Devices and add window shutters.""" """Iterate through all MAX! Devices and add window shutters."""
cube = hass.data[MAXCUBE_HANDLE].cube
devices = [] devices = []
for handler in hass.data[DATA_KEY].values():
cube = handler.cube
for device in cube.devices: for device in cube.devices:
name = "{} {}".format( name = "{} {}".format(
cube.room_by_id(device.room_id).name, device.name) cube.room_by_id(device.room_id).name, device.name)
# Only add Window Shutters # Only add Window Shutters
if cube.is_windowshutter(device): if cube.is_windowshutter(device):
devices.append(MaxCubeShutter(hass, name, device.rf_address)) devices.append(
MaxCubeShutter(handler, name, device.rf_address))
if devices: if devices:
add_devices(devices) add_devices(devices)
@ -33,12 +34,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MaxCubeShutter(BinarySensorDevice): class MaxCubeShutter(BinarySensorDevice):
"""Representation of a MAX! Cube Binary Sensor device.""" """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.""" """Initialize MAX! Cube BinarySensorDevice."""
self._name = name self._name = name
self._sensor_type = 'window' self._sensor_type = 'window'
self._rf_address = rf_address self._rf_address = rf_address
self._cubehandle = hass.data[MAXCUBE_HANDLE] self._cubehandle = handler
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
@property @property

View file

@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'bmw_connected_drive' DOMAIN = 'bmw_connected_drive'
CONF_REGION = 'region' CONF_REGION = 'region'
ATTR_VIN = 'vin'
ACCOUNT_SCHEMA = vol.Schema({ ACCOUNT_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
@ -35,35 +35,40 @@ CONFIG_SCHEMA = vol.Schema({
}, },
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_VIN): cv.string,
})
BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor'] BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor']
UPDATE_INTERVAL = 5 # in minutes 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.""" """Set up the BMW connected drive components."""
accounts = [] accounts = []
for name, account_config in config[DOMAIN].items(): for name, account_config in config[DOMAIN].items():
username = account_config[CONF_USERNAME] accounts.append(setup_account(account_config, hass, name))
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)
hass.data[DOMAIN] = accounts hass.data[DOMAIN] = accounts
for account in accounts: def _update_all(call) -> None:
account.update() """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: for component in BMW_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config) discovery.load_platform(hass, component, DOMAIN, {}, config)
@ -71,6 +76,48 @@ def setup(hass, config):
return True 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): class BMWConnectedDriveAccount(object):
"""Representation of a BMW vehicle.""" """Representation of a BMW vehicle."""

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

View file

@ -11,6 +11,7 @@ from datetime import timedelta
from homeassistant.components.calendar import CalendarEventDevice from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.google import ( from homeassistant.components.google import (
CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE, CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE,
CONF_IGNORE_AVAILABILITY, CONF_SEARCH,
GoogleCalendarService) GoogleCalendarService)
from homeassistant.util import Throttle, dt from homeassistant.util import Throttle, dt
@ -18,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_GOOGLE_SEARCH_PARAMS = { DEFAULT_GOOGLE_SEARCH_PARAMS = {
'orderBy': 'startTime', 'orderBy': 'startTime',
'maxResults': 1, 'maxResults': 5,
'singleEvents': True, 'singleEvents': True,
} }
@ -45,18 +46,22 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
def __init__(self, hass, calendar_service, calendar, data): def __init__(self, hass, calendar_service, calendar, data):
"""Create the Calendar event device.""" """Create the Calendar event device."""
self.data = GoogleCalendarData(calendar_service, calendar, self.data = GoogleCalendarData(calendar_service, calendar,
data.get('search', None)) data.get(CONF_SEARCH),
data.get(CONF_IGNORE_AVAILABILITY))
super().__init__(hass, data) super().__init__(hass, data)
class GoogleCalendarData(object): class GoogleCalendarData(object):
"""Class to utilize calendar service object to get next event.""" """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.""" """Set up how we are going to search the google calendar."""
self.calendar_service = calendar_service self.calendar_service = calendar_service
self.calendar_id = calendar_id self.calendar_id = calendar_id
self.search = search self.search = search
self.ignore_availability = ignore_availability
self.event = None self.event = None
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
@ -80,5 +85,17 @@ class GoogleCalendarData(object):
result = events.list(**params).execute() result = events.list(**params).execute()
items = result.get('items', []) 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 return True

View file

@ -1,21 +1,26 @@
# Describes the format for available calendar services # Describes the format for available calendar services
todoist: todoist_new_task:
new_task:
description: Create a new task and add it to a project. description: Create a new task and add it to a project.
fields: fields:
content: content:
description: The name of the task (Required). description: The name of the task.
example: Pick up the mail example: Pick up the mail
project: project:
description: The name of the project this task should belong to. Defaults to Inbox (Optional). description: The name of the project this task should belong to. Defaults to Inbox.
example: Errands example: Errands
labels: labels:
description: Any labels that you want to apply to this task, separated by a comma (Optional). description: Any labels that you want to apply to this task, separated by a comma.
example: Chores,Deliveries example: Chores,Deliveries
priority: priority:
description: The priority of this task, from 1 (normal) to 4 (urgent) (Optional). description: The priority of this task, from 1 (normal) to 4 (urgent).
example: 2 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: due_date:
description: The day this task is due, in format YYYY-MM-DD (Optional). description: The day this task is due, in format YYYY-MM-DD.
example: "2018-04-01" example: "2018-04-01"

View file

@ -41,6 +41,14 @@ CONTENT = 'content'
DESCRIPTION = 'description' DESCRIPTION = 'description'
# Calendar Platform: Used in the '_get_date()' method # Calendar Platform: Used in the '_get_date()' method
DATETIME = 'dateTime' 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? # Attribute: When is this task due?
# Service Call: When is this task due? # Service Call: When is this task due?
DUE_DATE = 'due_date' 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(PROJECT_NAME, default='inbox'): vol.All(cv.string, vol.Lower),
vol.Optional(LABELS): cv.ensure_list_csv, vol.Optional(LABELS): cv.ensure_list_csv,
vol.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)), 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({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -186,6 +198,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if PRIORITY in call.data: if PRIORITY in call.data:
item.update(priority=call.data[PRIORITY]) 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: if DUE_DATE in call.data:
due_date = dt.parse_datetime(call.data[DUE_DATE]) due_date = dt.parse_datetime(call.data[DUE_DATE])
if due_date is None: if due_date is None:

View file

@ -40,6 +40,7 @@ STATE_HEAT = 'heat'
STATE_COOL = 'cool' STATE_COOL = 'cool'
STATE_IDLE = 'idle' STATE_IDLE = 'idle'
STATE_AUTO = 'auto' STATE_AUTO = 'auto'
STATE_MANUAL = 'manual'
STATE_DRY = 'dry' STATE_DRY = 'dry'
STATE_FAN_ONLY = 'fan_only' STATE_FAN_ONLY = 'fan_only'
STATE_ECO = 'eco' STATE_ECO = 'eco'

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

View file

@ -38,7 +38,10 @@ class HiveClimateEntity(ClimateDevice):
self.node_id = hivedevice["Hive_NodeID"] self.node_id = hivedevice["Hive_NodeID"]
self.node_name = hivedevice["Hive_NodeName"] self.node_name = hivedevice["Hive_NodeName"]
self.device_type = hivedevice["HA_DeviceType"] self.device_type = hivedevice["HA_DeviceType"]
if self.device_type == "Heating":
self.thermostat_node_id = hivedevice["Thermostat_NodeID"]
self.session = hivesession self.session = hivesession
self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type, self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id) self.node_id)
@ -71,6 +74,11 @@ class HiveClimateEntity(ClimateDevice):
friendly_name = "Hot Water" friendly_name = "Hot Water"
return friendly_name return friendly_name
@property
def device_state_attributes(self):
"""Show Device Attributes."""
return self.attributes
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
@ -175,4 +183,9 @@ class HiveClimateEntity(ClimateDevice):
def update(self): def update(self):
"""Update all Node data from Hive.""" """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.session.core.update_data(self.node_id)
self.attributes = self.session.attributes.state_attributes(node)

View file

@ -10,7 +10,7 @@ import logging
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE, ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE) 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 from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
_LOGGER = logging.getLogger(__name__) _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): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Iterate through all MAX! Devices and add thermostats.""" """Iterate through all MAX! Devices and add thermostats."""
cube = hass.data[MAXCUBE_HANDLE].cube
devices = [] devices = []
for handler in hass.data[DATA_KEY].values():
cube = handler.cube
for device in cube.devices: for device in cube.devices:
name = '{} {}'.format( name = '{} {}'.format(
cube.room_by_id(device.room_id).name, device.name) cube.room_by_id(device.room_id).name, device.name)
if cube.is_thermostat(device) or cube.is_wallthermostat(device): if cube.is_thermostat(device) or cube.is_wallthermostat(device):
devices.append(MaxCubeClimate(hass, name, device.rf_address)) devices.append(
MaxCubeClimate(handler, name, device.rf_address))
if devices: if devices:
add_devices(devices) add_devices(devices)
@ -42,14 +42,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MaxCubeClimate(ClimateDevice): class MaxCubeClimate(ClimateDevice):
"""MAX! Cube ClimateDevice.""" """MAX! Cube ClimateDevice."""
def __init__(self, hass, name, rf_address): def __init__(self, handler, name, rf_address):
"""Initialize MAX! Cube ClimateDevice.""" """Initialize MAX! Cube ClimateDevice."""
self._name = name self._name = name
self._unit_of_measurement = TEMP_CELSIUS self._unit_of_measurement = TEMP_CELSIUS
self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST, self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST,
STATE_VACATION] STATE_VACATION]
self._rf_address = rf_address self._rf_address = rf_address
self._cubehandle = hass.data[MAXCUBE_HANDLE] self._cubehandle = handler
@property @property
def supported_features(self): def supported_features(self):

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

View file

@ -187,6 +187,11 @@ class NestThermostat(ClimateDevice):
device_mode = operation_mode device_mode = operation_mode
elif operation_mode == STATE_AUTO: elif operation_mode == STATE_AUTO:
device_mode = NEST_MODE_HEAT_COOL 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 self.device.mode = device_mode
@property @property

View file

@ -1,11 +1,10 @@
"""Http views to control the config manager.""" """Http views to control the config manager."""
import asyncio import asyncio
import voluptuous as vol from homeassistant import config_entries, data_entry_flow
from homeassistant import config_entries
from homeassistant.components.http import HomeAssistantView 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'] REQUIREMENTS = ['voluptuous-serialize==1']
@ -16,15 +15,17 @@ def async_setup(hass):
"""Enable the Home Assistant views.""" """Enable the Home Assistant views."""
hass.http.register_view(ConfigManagerEntryIndexView) hass.http.register_view(ConfigManagerEntryIndexView)
hass.http.register_view(ConfigManagerEntryResourceView) hass.http.register_view(ConfigManagerEntryResourceView)
hass.http.register_view(ConfigManagerFlowIndexView) hass.http.register_view(
hass.http.register_view(ConfigManagerFlowResourceView) ConfigManagerFlowIndexView(hass.config_entries.flow))
hass.http.register_view(
ConfigManagerFlowResourceView(hass.config_entries.flow))
hass.http.register_view(ConfigManagerAvailableFlowView) hass.http.register_view(ConfigManagerAvailableFlowView)
return True return True
def _prepare_json(result): def _prepare_json(result):
"""Convert result for JSON.""" """Convert result for JSON."""
if result['type'] != config_entries.RESULT_TYPE_FORM: if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
return result return result
import voluptuous_serialize import voluptuous_serialize
@ -78,7 +79,7 @@ class ConfigManagerEntryResourceView(HomeAssistantView):
return self.json(result) return self.json(result)
class ConfigManagerFlowIndexView(HomeAssistantView): class ConfigManagerFlowIndexView(FlowManagerIndexView):
"""View to create config flows.""" """View to create config flows."""
url = '/api/config/config_entries/flow' url = '/api/config/config_entries/flow'
@ -94,81 +95,16 @@ class ConfigManagerFlowIndexView(HomeAssistantView):
hass = request.app['hass'] hass = request.app['hass']
return self.json([ return self.json([
flow for flow in hass.config_entries.flow.async_progress() flw for flw in hass.config_entries.flow.async_progress()
if flow['source'] != config_entries.SOURCE_USER]) if flw['source'] != data_entry_flow.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)
class ConfigManagerFlowResourceView(HomeAssistantView): class ConfigManagerFlowResourceView(FlowManagerResourceView):
"""View to interact with the flow manager.""" """View to interact with the flow manager."""
url = '/api/config/config_entries/flow/{flow_id}' url = '/api/config/config_entries/flow/{flow_id}'
name = 'api:config:config_entries:flow:resource' 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): class ConfigManagerAvailableFlowView(HomeAssistantView):
"""View to query available flows.""" """View to query available flows."""

View file

@ -18,30 +18,31 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_DISTANCE_SENSOR = "distance_sensor" ATTR_DISTANCE_SENSOR = 'distance_sensor'
ATTR_DOOR_STATE = "door_state" ATTR_DOOR_STATE = 'door_state'
ATTR_SIGNAL_STRENGTH = "wifi_signal" ATTR_SIGNAL_STRENGTH = 'wifi_signal'
CONF_DEVICEKEY = "device_key" CONF_DEVICE_ID = 'device_id'
CONF_DEVICE_KEY = 'device_key'
DEFAULT_NAME = 'OpenGarage' DEFAULT_NAME = 'OpenGarage'
DEFAULT_PORT = 80 DEFAULT_PORT = 80
STATE_CLOSING = "closing" STATE_CLOSING = 'closing'
STATE_OFFLINE = "offline" STATE_OFFLINE = 'offline'
STATE_OPENING = "opening" STATE_OPENING = 'opening'
STATE_STOPPED = "stopped" STATE_STOPPED = 'stopped'
STATES_MAP = { STATES_MAP = {
0: STATE_CLOSED, 0: STATE_CLOSED,
1: STATE_OPEN 1: STATE_OPEN,
} }
COVER_SCHEMA = vol.Schema({ COVER_SCHEMA = vol.Schema({
vol.Required(CONF_DEVICEKEY): cv.string, vol.Required(CONF_DEVICE_KEY): cv.string,
vol.Required(CONF_HOST): 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_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_NAME): cv.string
}) })
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -50,7 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up OpenGarage covers.""" """Set up the OpenGarage covers."""
covers = [] covers = []
devices = config.get(CONF_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_NAME: device_config.get(CONF_NAME),
CONF_HOST: device_config.get(CONF_HOST), CONF_HOST: device_config.get(CONF_HOST),
CONF_PORT: device_config.get(CONF_PORT), CONF_PORT: device_config.get(CONF_PORT),
"device_id": device_config.get(CONF_DEVICE, device_id), CONF_DEVICE_ID: device_config.get(CONF_DEVICE, device_id),
CONF_DEVICEKEY: device_config.get(CONF_DEVICEKEY) CONF_DEVICE_KEY: device_config.get(CONF_DEVICE_KEY)
} }
covers.append(OpenGarageCover(hass, args)) covers.append(OpenGarageCover(hass, args))
@ -79,8 +80,8 @@ class OpenGarageCover(CoverDevice):
self.hass = hass self.hass = hass
self._name = args[CONF_NAME] self._name = args[CONF_NAME]
self.device_id = args['device_id'] self.device_id = args['device_id']
self._devicekey = args[CONF_DEVICEKEY] self._device_key = args[CONF_DEVICE_KEY]
self._state = STATE_UNKNOWN self._state = None
self._state_before_move = None self._state_before_move = None
self.dist = None self.dist = None
self.signal = None self.signal = None
@ -138,8 +139,8 @@ class OpenGarageCover(CoverDevice):
try: try:
status = self._get_status() status = self._get_status()
if self._name is None: if self._name is None:
if status["name"] is not None: if status['name'] is not None:
self._name = status["name"] self._name = status['name']
state = STATES_MAP.get(status.get('door'), STATE_UNKNOWN) state = STATES_MAP.get(status.get('door'), STATE_UNKNOWN)
if self._state_before_move is not None: if self._state_before_move is not None:
if self._state_before_move != state: if self._state_before_move != state:
@ -152,7 +153,7 @@ class OpenGarageCover(CoverDevice):
self.signal = status.get('rssi') self.signal = status.get('rssi')
self.dist = status.get('dist') self.dist = status.get('dist')
self._available = True 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", _LOGGER.error("Unable to connect to OpenGarage device: %(reason)s",
dict(reason=ex)) dict(reason=ex))
self._state = STATE_OFFLINE self._state = STATE_OFFLINE
@ -166,15 +167,15 @@ class OpenGarageCover(CoverDevice):
def _push_button(self): def _push_button(self):
"""Send commands to API.""" """Send commands to API."""
url = '{}/cc?dkey={}&click=1'.format( url = '{}/cc?dkey={}&click=1'.format(
self.opengarage_url, self._devicekey) self.opengarage_url, self._device_key)
try: try:
response = requests.get(url, timeout=10).json() response = requests.get(url, timeout=10).json()
if response["result"] == 2: if response['result'] == 2:
_LOGGER.error("Unable to control %s: device_key is incorrect.", _LOGGER.error("Unable to control %s: Device key is incorrect",
self._name) self._name)
self._state = self._state_before_move self._state = self._state_before_move
self._state_before_move = None 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", _LOGGER.error("Unable to connect to OpenGarage device: %(reason)s",
dict(reason=ex)) dict(reason=ex))
self._state = self._state_before_move self._state = self._state_before_move

View file

@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None): 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'] controller = hass.data[TAHOMA_DOMAIN]['controller']
devices = [] devices = []
for device in hass.data[TAHOMA_DOMAIN]['devices']['cover']: for device in hass.data[TAHOMA_DOMAIN]['devices']['cover']:

View file

@ -18,6 +18,7 @@
"no_key": "Couldn't get an API key" "no_key": "Couldn't get an API key"
}, },
"abort": { "abort": {
"already_configured": "Bridge is already configured",
"no_bridges": "No deCONZ bridges discovered", "no_bridges": "No deCONZ bridges discovered",
"one_instance_only": "Component only supports one deCONZ instance" "one_instance_only": "Component only supports one deCONZ instance"
} }

View file

@ -4,28 +4,20 @@ Support for deCONZ devices.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/deconz/ https://home-assistant.io/components/deconz/
""" """
import logging
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.discovery import SERVICE_DECONZ
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import (
from homeassistant.helpers import discovery, aiohttp_client aiohttp_client, discovery, config_validation as cv)
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.json import load_json
from homeassistant.util.json import load_json, save_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__) REQUIREMENTS = ['pydeconz==36']
DOMAIN = 'deconz'
DATA_DECONZ_ID = 'deconz_entities'
CONFIG_FILE = 'deconz.conf'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -46,44 +38,36 @@ 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): async def async_setup(hass, config):
"""Set up services and configuration for deCONZ component.""" """Load configuration for deCONZ component.
result = False
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( config_file = await hass.async_add_job(
load_json, hass.config.path(CONFIG_FILE)) 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: if config_file:
result = await async_setup_deconz(hass, config, config_file) deconz_config = config_file
elif CONF_HOST in config[DOMAIN]:
if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]:
deconz_config = config[DOMAIN] deconz_config = config[DOMAIN]
if CONF_API_KEY in deconz_config: if deconz_config and not configured_hosts(hass):
result = await async_setup_deconz(hass, config, deconz_config) hass.async_add_job(hass.config_entries.flow.async_init(
else: DOMAIN, source='import', data=deconz_config
await async_request_configuration(hass, config, deconz_config) ))
return True return True
if not result:
discovery.async_listen(hass, SERVICE_DECONZ, async_deconz_discovered)
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 True
return False
async def async_setup_deconz(hass, config, deconz_config): async def async_setup_deconz(hass, config, deconz_config):
@ -94,8 +78,8 @@ async def async_setup_deconz(hass, config, deconz_config):
""" """
_LOGGER.debug("deCONZ config %s", deconz_config) _LOGGER.debug("deCONZ config %s", deconz_config)
from pydeconz import DeconzSession from pydeconz import DeconzSession
websession = async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, websession, **deconz_config) deconz = DeconzSession(hass.loop, session, **deconz_config)
result = await deconz.async_load_parameters() result = await deconz.async_load_parameters()
if result is False: if result is False:
_LOGGER.error("Failed to communicate with deCONZ") _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) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown)
return True 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

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

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

View file

@ -18,6 +18,7 @@
"no_key": "Couldn't get an API key" "no_key": "Couldn't get an API key"
}, },
"abort": { "abort": {
"already_configured": "Bridge is already configured",
"no_bridges": "No deCONZ bridges discovered", "no_bridges": "No deCONZ bridges discovered",
"one_instance_only": "Component only supports one deCONZ instance" "one_instance_only": "Component only supports one deCONZ instance"
} }

View file

@ -605,6 +605,17 @@ class DeviceScanner(object):
""" """
return self.hass.async_add_job(self.get_device_name, device) 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): def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
"""Load devices from YAML configuration file.""" """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) host_name = yield from scanner.async_get_device_name(mac)
seen.add(mac) seen.add(mac)
try:
extra_attributes = (yield from
scanner.async_get_extra_attributes(mac))
except NotImplementedError:
extra_attributes = dict()
kwargs = { kwargs = {
'mac': mac, 'mac': mac,
'host_name': host_name, '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) zone_home = hass.states.get(zone.ENTITY_ID_HOME)

View file

@ -48,8 +48,11 @@ class BMWDeviceTracker(object):
return return
_LOGGER.debug('Updating %s', dev_id) _LOGGER.debug('Updating %s', dev_id)
attrs = {
'vin': self.vehicle.vin,
}
self._see( self._see(
dev_id=dev_id, host_name=self.vehicle.name, 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'
) )

View file

@ -19,7 +19,7 @@ from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['locationsharinglib==0.4.0'] REQUIREMENTS = ['locationsharinglib==1.2.1']
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'

View file

@ -176,7 +176,7 @@ class MikrotikScanner(DeviceScanner):
for device in device_names for device in device_names
if device.get('mac-address')} if device.get('mac-address')}
if self.wireless_exist: if self.wireless_exist or self.capsman_exist:
self.last_results = { self.last_results = {
device.get('mac-address'): device.get('mac-address'):
mac_names.get(device.get('mac-address')) mac_names.get(device.get('mac-address'))

View file

@ -80,6 +80,8 @@ class NmapDeviceScanner(DeviceScanner):
"""Scan for new devices and return a list with found device IDs.""" """Scan for new devices and return a list with found device IDs."""
self._update_info() self._update_info()
_LOGGER.debug("Nmap last results %s", self.last_results)
return [device.mac for device in self.last_results] return [device.mac for device in self.last_results]
def get_device_name(self, device): def get_device_name(self, device):
@ -91,6 +93,13 @@ class NmapDeviceScanner(DeviceScanner):
return filter_named[0] return filter_named[0]
return None 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): def _update_info(self):
"""Scan the network for devices. """Scan the network for devices.

View file

@ -103,6 +103,9 @@ class UbusDeviceScanner(DeviceScanner):
"""Return the name of the given device or None if we don't know.""" """Return the name of the given device or None if we don't know."""
if self.mac2name is None: if self.mac2name is None:
self._generate_mac2name() self._generate_mac2name()
if self.mac2name is None:
# Generation of mac2name dictionary failed
return None
name = self.mac2name.get(device.upper(), None) name = self.mac2name.get(device.upper(), None)
return name return name

View file

@ -122,3 +122,9 @@ class UnifiScanner(DeviceScanner):
name = client.get('name') or client.get('hostname') name = client.get('name') or client.get('hostname')
_LOGGER.debug("Device mac %s name %s", device, name) _LOGGER.debug("Device mac %s name %s", device, name)
return 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

View file

@ -20,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), 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): def get_scanner(hass, config):
@ -41,7 +41,7 @@ def get_scanner(hass, config):
device_info.model, device_info.model,
device_info.firmware_version, device_info.firmware_version,
device_info.hardware_version) device_info.hardware_version)
scanner = XiaomiMiioDeviceScanner(hass, device) scanner = XiaomiMiioDeviceScanner(device)
except DeviceException as ex: except DeviceException as ex:
_LOGGER.error("Device unavailable or token incorrect: %s", ex) _LOGGER.error("Device unavailable or token incorrect: %s", ex)
@ -51,7 +51,7 @@ def get_scanner(hass, config):
class XiaomiMiioDeviceScanner(DeviceScanner): class XiaomiMiioDeviceScanner(DeviceScanner):
"""This class queries a Xiaomi Mi WiFi Repeater.""" """This class queries a Xiaomi Mi WiFi Repeater."""
def __init__(self, hass, device): def __init__(self, device):
"""Initialize the scanner.""" """Initialize the scanner."""
self.device = device self.device = device

View file

@ -13,7 +13,7 @@ import os
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import data_entry_flow
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.const import EVENT_HOMEASSISTANT_START
import homeassistant.helpers.config_validation as cv 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 from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==1.3.0'] REQUIREMENTS = ['netdisco==1.3.1']
DOMAIN = 'discovery' DOMAIN = 'discovery'
@ -40,8 +40,10 @@ SERVICE_HUE = 'philips_hue'
SERVICE_DECONZ = 'deconz' SERVICE_DECONZ = 'deconz'
SERVICE_DAIKIN = 'daikin' SERVICE_DAIKIN = 'daikin'
SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
SERVICE_HOMEKIT = 'homekit'
CONFIG_ENTRY_HANDLERS = { CONFIG_ENTRY_HANDLERS = {
SERVICE_DECONZ: 'deconz',
SERVICE_HUE: 'hue', SERVICE_HUE: 'hue',
} }
@ -56,7 +58,6 @@ SERVICE_HANDLERS = {
SERVICE_WINK: ('wink', None), SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None), SERVICE_TELLDUSLIVE: ('tellduslive', None),
SERVICE_DECONZ: ('deconz', None),
SERVICE_DAIKIN: ('daikin', None), SERVICE_DAIKIN: ('daikin', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
'google_cast': ('media_player', 'cast'), 'google_cast': ('media_player', 'cast'),
@ -77,15 +78,23 @@ SERVICE_HANDLERS = {
'bose_soundtouch': ('media_player', 'soundtouch'), 'bose_soundtouch': ('media_player', 'soundtouch'),
'bluesound': ('media_player', 'bluesound'), 'bluesound': ('media_player', 'bluesound'),
'songpal': ('media_player', 'songpal'), 'songpal': ('media_player', 'songpal'),
'kodi': ('media_player', 'kodi'),
}
OPTIONAL_SERVICE_HANDLERS = {
SERVICE_HOMEKIT: ('homekit_controller', None),
} }
CONF_IGNORE = 'ignore' CONF_IGNORE = 'ignore'
CONF_ENABLE = 'enable'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
vol.Required(DOMAIN): vol.Schema({ vol.Required(DOMAIN): vol.Schema({
vol.Optional(CONF_IGNORE, default=[]): vol.Optional(CONF_IGNORE, default=[]):
vol.All(cv.ensure_list, [ 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) }, extra=vol.ALLOW_EXTRA)
@ -104,6 +113,9 @@ async def async_setup(hass, config):
# Platforms ignore by config # Platforms ignore by config
ignored_platforms = config[DOMAIN][CONF_IGNORE] ignored_platforms = config[DOMAIN][CONF_IGNORE]
# Optional platforms enabled by config
enabled_platforms = config[DOMAIN][CONF_ENABLE]
async def new_service_found(service, info): async def new_service_found(service, info):
"""Handle a new service if one is found.""" """Handle a new service if one is found."""
if service in ignored_platforms: if service in ignored_platforms:
@ -119,13 +131,16 @@ async def async_setup(hass, config):
if service in CONFIG_ENTRY_HANDLERS: if service in CONFIG_ENTRY_HANDLERS:
await hass.config_entries.flow.async_init( await hass.config_entries.flow.async_init(
CONFIG_ENTRY_HANDLERS[service], CONFIG_ENTRY_HANDLERS[service],
source=config_entries.SOURCE_DISCOVERY, source=data_entry_flow.SOURCE_DISCOVERY,
data=info data=info
) )
return return
comp_plat = SERVICE_HANDLERS.get(service) 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. # We do not know how to handle this service.
if not comp_plat: if not comp_plat:
logger.info("Unknown service discovered: %s %s", service, info) logger.info("Unknown service discovered: %s %s", service, info)

View file

@ -16,7 +16,7 @@ from homeassistant.const import CONF_API_KEY
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.util.json import save_json from homeassistant.util.json import save_json
REQUIREMENTS = ['python-ecobee-api==0.0.17'] REQUIREMENTS = ['python-ecobee-api==0.0.18']
_CONFIGURING = {} _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View 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

View 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

View file

@ -24,7 +24,7 @@ from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20180404.0'] REQUIREMENTS = ['home-assistant-frontend==20180420.0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']

View file

@ -44,6 +44,7 @@ CONF_ENTITIES = 'entities'
CONF_TRACK = 'track' CONF_TRACK = 'track'
CONF_SEARCH = 'search' CONF_SEARCH = 'search'
CONF_OFFSET = 'offset' CONF_OFFSET = 'offset'
CONF_IGNORE_AVAILABILITY = 'ignore_availability'
DEFAULT_CONF_TRACK_NEW = True DEFAULT_CONF_TRACK_NEW = True
DEFAULT_CONF_OFFSET = '!!' DEFAULT_CONF_OFFSET = '!!'
@ -74,8 +75,9 @@ _SINGLE_CALSEARCH_CONFIG = vol.Schema({
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_DEVICE_ID): cv.string, vol.Required(CONF_DEVICE_ID): cv.string,
vol.Optional(CONF_TRACK): cv.boolean, 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_OFFSET): cv.string,
vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean,
}) })
DEVICE_SCHEMA = vol.Schema({ DEVICE_SCHEMA = vol.Schema({

View file

@ -35,7 +35,7 @@ CONF_TYPES = 'types'
ICON_UNKNOWN = 'mdi:help' ICON_UNKNOWN = 'mdi:help'
ICON_AUDIO = 'mdi:speaker' ICON_AUDIO = 'mdi:speaker'
ICON_PLAYER = 'mdi:play' ICON_PLAYER = 'mdi:play'
ICON_TUNER = 'mdi:nest-thermostat' ICON_TUNER = 'mdi:radio'
ICON_RECORDER = 'mdi:microphone' ICON_RECORDER = 'mdi:microphone'
ICON_TV = 'mdi:television' ICON_TV = 'mdi:television'
ICONS_BY_TYPE = { ICONS_BY_TYPE = {

View file

@ -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) 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, def get_states(hass, utc_point_in_time, entity_ids=None, run=None,
filters=None): filters=None):
"""Return the states at a specific point in time.""" """Return the states at a specific point in time."""

View file

@ -12,7 +12,7 @@ from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL,
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.discovery import load_platform
REQUIREMENTS = ['pyhiveapi==0.2.11'] REQUIREMENTS = ['pyhiveapi==0.2.14']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'hive' DOMAIN = 'hive'
@ -44,6 +44,8 @@ class HiveSession:
light = None light = None
sensor = None sensor = None
switch = None switch = None
weather = None
attributes = None
def setup(hass, config): def setup(hass, config):
@ -70,6 +72,8 @@ def setup(hass, config):
session.hotwater = Pyhiveapi.Hotwater() session.hotwater = Pyhiveapi.Hotwater()
session.light = Pyhiveapi.Light() session.light = Pyhiveapi.Light()
session.switch = Pyhiveapi.Switch() session.switch = Pyhiveapi.Switch()
session.weather = Pyhiveapi.Weather()
session.attributes = Pyhiveapi.Attributes()
hass.data[DATA_HIVE] = session hass.data[DATA_HIVE] = session
for ha_type, hive_type in DEVICETYPES.items(): for ha_type, hive_type in DEVICETYPES.items():

View file

@ -8,12 +8,11 @@ from zlib import adler32
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import ( from homeassistant.components.cover import (
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION)
from homeassistant.components.cover import SUPPORT_SET_POSITION
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_DEVICE_CLASS, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA 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 homeassistant.util.decorator import Registry
from .const import ( from .const import (
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, 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 ( from .util import (
validate_entity_config, show_setup_message) validate_entity_config, show_setup_message)
TYPES = Registry() TYPES = Registry()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['HAP-python==1.1.8'] REQUIREMENTS = ['HAP-python==1.1.9']
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
@ -79,56 +80,65 @@ def get_accessory(hass, state, aid, config):
state.entity_id) state.entity_id)
return None return None
if state.domain == 'sensor': a_type = None
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) config = config or {}
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)
elif state.domain == 'cover': if state.domain == 'alarm_control_panel':
# Only add covers that support set_cover_position a_type = 'SecuritySystem'
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)
elif state.domain == 'alarm_control_panel': elif state.domain == 'binary_sensor' or state.domain == 'device_tracker':
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem') a_type = 'BinarySensor'
return TYPES['SecuritySystem'](hass, state.entity_id, state.name,
alarm_code=config.get(ATTR_CODE),
aid=aid)
elif state.domain == 'climate': elif state.domain == 'climate':
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) a_type = 'Thermostat'
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)
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat') elif state.domain == 'cover':
return TYPES['Thermostat'](hass, state.entity_id, features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
state.name, support_auto, aid=aid) 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': elif state.domain == 'light':
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light') a_type = 'Light'
return TYPES['Light'](hass, state.entity_id, state.name, aid=aid)
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' \ elif state.domain == 'switch' or state.domain == 'remote' \
or state.domain == 'input_boolean' or state.domain == 'script': or state.domain == 'input_boolean' or state.domain == 'script':
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') a_type = 'Switch'
return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid)
if a_type is None:
return 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): def generate_aid(entity_id):
"""Generate accessory aid with zlib adler32.""" """Generate accessory aid with zlib adler32."""
@ -143,7 +153,7 @@ class HomeKit():
def __init__(self, hass, port, entity_filter, entity_config): def __init__(self, hass, port, entity_filter, entity_config):
"""Initialize a HomeKit object.""" """Initialize a HomeKit object."""
self._hass = hass self.hass = hass
self._port = port self._port = port
self._filter = entity_filter self._filter = entity_filter
self._config = entity_config self._config = entity_config
@ -156,11 +166,11 @@ class HomeKit():
"""Setup bridge and accessory driver.""" """Setup bridge and accessory driver."""
from .accessories import HomeBridge, HomeDriver from .accessories import HomeBridge, HomeDriver
self._hass.bus.async_listen_once( self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, self.stop) EVENT_HOMEASSISTANT_STOP, self.stop)
path = self._hass.config.path(HOMEKIT_FILE) path = self.hass.config.path(HOMEKIT_FILE)
self.bridge = HomeBridge(self._hass) self.bridge = HomeBridge(self.hass)
self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path) self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path)
def add_bridge_accessory(self, state): def add_bridge_accessory(self, state):
@ -169,7 +179,7 @@ class HomeKit():
return return
aid = generate_aid(state.entity_id) aid = generate_aid(state.entity_id)
conf = self._config.pop(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: if acc is not None:
self.bridge.add_accessory(acc) self.bridge.add_accessory(acc)
@ -181,15 +191,15 @@ class HomeKit():
# pylint: disable=unused-variable # pylint: disable=unused-variable
from . import ( # noqa F401 from . import ( # noqa F401
type_covers, type_lights, type_security_systems, type_sensors, type_covers, type_lights, type_locks, type_security_systems,
type_switches, type_thermostats) 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.add_bridge_accessory(state)
self.bridge.set_broker(self.driver) self.bridge.set_broker(self.driver)
if not self.bridge.paired: if not self.bridge.paired:
show_setup_message(self.bridge, self._hass) show_setup_message(self.hass, self.bridge)
_LOGGER.debug('Driver start') _LOGGER.debug('Driver start')
self.driver.start() self.driver.start()

View file

@ -1,21 +1,64 @@
"""Extend the basic Accessory and Bridge functions.""" """Extend the basic Accessory and Bridge functions."""
from datetime import timedelta
from functools import wraps
from inspect import getmodule
import logging import logging
from pyhap.accessory import Accessory, Bridge, Category from pyhap.accessory import Accessory, Bridge, Category
from pyhap.accessory_driver import AccessoryDriver 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 ( from .const import (
ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER,
MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER,
CHAR_NAME, CHAR_SERIAL_NUMBER) CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
from .util import ( from .util import (
show_setup_message, dismiss_setup_message) show_setup_message, dismiss_setup_message)
_LOGGER = logging.getLogger(__name__) _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): def add_preload_service(acc, service, chars=None):
"""Define and return a service to be available for the accessory.""" """Define and return a service to be available for the accessory."""
from pyhap.loader import get_serv_loader, get_char_loader from pyhap.loader import get_serv_loader, get_char_loader
@ -29,6 +72,18 @@ def add_preload_service(acc, service, chars=None):
return service 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, def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
serial_number='0000'): serial_number='0000'):
"""Set the default accessory information.""" """Set the default accessory information."""
@ -42,14 +97,13 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
class HomeAccessory(Accessory): class HomeAccessory(Accessory):
"""Adapter class for Accessory.""" """Adapter class for Accessory."""
# pylint: disable=no-member def __init__(self, hass, name, entity_id, aid, category):
def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL,
category='OTHER', **kwargs):
"""Initialize a Accessory object.""" """Initialize a Accessory object."""
super().__init__(name, **kwargs) super().__init__(name, aid=aid)
set_accessory_info(self, name, model) set_accessory_info(self, name, model=entity_id)
self.category = getattr(Category, category, Category.OTHER) self.category = getattr(Category, category, Category.OTHER)
self.entity_id = entity_id
self.hass = hass
def _set_services(self): def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO) add_preload_service(self, SERV_ACCESSORY_INFO)
@ -57,19 +111,33 @@ class HomeAccessory(Accessory):
def run(self): def run(self):
"""Method called by accessory after driver is started.""" """Method called by accessory after driver is started."""
state = self.hass.states.get(self.entity_id) 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( 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): class HomeBridge(Bridge):
"""Adapter class for Bridge.""" """Adapter class for Bridge."""
def __init__(self, hass, name=BRIDGE_NAME, def __init__(self, hass, name=BRIDGE_NAME):
model=BRIDGE_MODEL, **kwargs):
"""Initialize a Bridge object.""" """Initialize a Bridge object."""
super().__init__(name, **kwargs) super().__init__(name)
set_accessory_info(self, name, model) set_accessory_info(self, name, model=BRIDGE_MODEL)
self.hass = hass self.hass = hass
def _set_services(self): def _set_services(self):
@ -87,7 +155,7 @@ class HomeBridge(Bridge):
def remove_paired_client(self, client_uuid): def remove_paired_client(self, client_uuid):
"""Override super function to show setup message if unpaired.""" """Override super function to show setup message if unpaired."""
super().remove_paired_client(client_uuid) super().remove_paired_client(client_uuid)
show_setup_message(self, self.hass) show_setup_message(self.hass, self)
class HomeDriver(AccessoryDriver): class HomeDriver(AccessoryDriver):

View file

@ -1,5 +1,6 @@
"""Constants used be the HomeKit component.""" """Constants used be the HomeKit component."""
# #### MISC #### # #### MISC ####
DEBOUNCE_TIMEOUT = 0.5
DOMAIN = 'homekit' DOMAIN = 'homekit'
HOMEKIT_FILE = '.homekit.state' HOMEKIT_FILE = '.homekit.state'
HOMEKIT_NOTIFY_ID = 4663548 HOMEKIT_NOTIFY_ID = 4663548
@ -17,15 +18,15 @@ DEFAULT_PORT = 51827
SERVICE_HOMEKIT_START = 'start' SERVICE_HOMEKIT_START = 'start'
# #### STRING CONSTANTS #### # #### STRING CONSTANTS ####
ACCESSORY_MODEL = 'homekit.accessory'
ACCESSORY_NAME = 'Home Accessory'
BRIDGE_MODEL = 'homekit.bridge' BRIDGE_MODEL = 'homekit.bridge'
BRIDGE_NAME = 'Home Assistant' BRIDGE_NAME = 'Home Assistant'
MANUFACTURER = 'HomeAssistant' MANUFACTURER = 'HomeAssistant'
# #### Categories #### # #### Categories ####
CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM' CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM'
CATEGORY_GARAGE_DOOR_OPENER = 'GARAGE_DOOR_OPENER'
CATEGORY_LIGHT = 'LIGHTBULB' CATEGORY_LIGHT = 'LIGHTBULB'
CATEGORY_LOCK = 'DOOR_LOCK'
CATEGORY_SENSOR = 'SENSOR' CATEGORY_SENSOR = 'SENSOR'
CATEGORY_SWITCH = 'SWITCH' CATEGORY_SWITCH = 'SWITCH'
CATEGORY_THERMOSTAT = 'THERMOSTAT' CATEGORY_THERMOSTAT = 'THERMOSTAT'
@ -34,40 +35,80 @@ CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING'
# #### Services #### # #### Services ####
SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_ACCESSORY_INFO = 'AccessoryInformation'
SERV_HUMIDITY_SENSOR = 'HumiditySensor' SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor'
# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered, SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor'
# StatusLowBattery, Name 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_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name
SERV_LOCK = 'LockMechanism'
SERV_MOTION_SENSOR = 'MotionSensor'
SERV_OCCUPANCY_SENSOR = 'OccupancySensor'
SERV_SECURITY_SYSTEM = 'SecuritySystem' SERV_SECURITY_SYSTEM = 'SecuritySystem'
SERV_SMOKE_SENSOR = 'SmokeSensor'
SERV_SWITCH = 'Switch' SERV_SWITCH = 'Switch'
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
SERV_THERMOSTAT = 'Thermostat' SERV_THERMOSTAT = 'Thermostat'
SERV_WINDOW_COVERING = 'WindowCovering' SERV_WINDOW_COVERING = 'WindowCovering'
# CurrentPosition, TargetPosition, PositionState
# #### Characteristics #### # #### Characteristics ####
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
CHAR_AIR_QUALITY = 'AirQuality'
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] 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_COLOR_TEMPERATURE = 'ColorTemperature'
CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState'
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel'
CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState'
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
CHAR_CURRENT_POSITION = 'CurrentPosition' CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100]
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
CHAR_HUE = 'Hue' # arcdegress | [0, 360] 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_MANUFACTURER = 'Manufacturer'
CHAR_MODEL = 'Model' CHAR_MODEL = 'Model'
CHAR_MOTION_DETECTED = 'MotionDetected'
CHAR_NAME = 'Name' CHAR_NAME = 'Name'
CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected'
CHAR_ON = 'On' # boolean CHAR_ON = 'On' # boolean
CHAR_POSITION_STATE = 'PositionState' CHAR_POSITION_STATE = 'PositionState'
CHAR_SATURATION = 'Saturation' # percent CHAR_SATURATION = 'Saturation' # percent
CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SERIAL_NUMBER = 'SerialNumber'
CHAR_SMOKE_DETECTED = 'SmokeDetected'
CHAR_TARGET_DOOR_STATE = 'TargetDoorState'
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
CHAR_TARGET_POSITION = 'TargetPosition' CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100]
CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
# #### Properties #### # #### Properties ####
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} 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'

View file

@ -1,18 +1,67 @@
"""Class to hold all cover accessories.""" """Class to hold all cover accessories."""
import logging 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 . import TYPES
from .accessories import HomeAccessory, add_preload_service from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import ( from .const import (
CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING, 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__) _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') @TYPES.register('WindowCovering')
class WindowCovering(HomeAccessory): class WindowCovering(HomeAccessory):
"""Generate a Window accessory for a cover entity. """Generate a Window accessory for a cover entity.
@ -20,54 +69,91 @@ class WindowCovering(HomeAccessory):
The cover entity must support: set_cover_position. 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.""" """Initialize a WindowCovering accessory object."""
super().__init__(display_name, entity_id, super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
CATEGORY_WINDOW_COVERING, **kwargs)
self.hass = hass
self.entity_id = entity_id
self.current_position = None
self.homekit_target = None self.homekit_target = None
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = serv_cover. \ self.char_current_position = setup_char(
get_characteristic(CHAR_CURRENT_POSITION) CHAR_CURRENT_POSITION, serv_cover, value=0)
self.char_target_position = serv_cover. \ self.char_target_position = setup_char(
get_characteristic(CHAR_TARGET_POSITION) CHAR_TARGET_POSITION, serv_cover, value=0,
self.char_position_state = serv_cover. \ callback=self.move_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
def move_cover(self, value): def move_cover(self, value):
"""Move cover to value if call came from HomeKit.""" """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) _LOGGER.debug('%s: Set position to %d', self.entity_id, value)
self.homekit_target = 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)
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.""" """Update cover position after state changed."""
if new_state is None:
return
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
if isinstance(current_position, int): if isinstance(current_position, int):
self.current_position = current_position self.char_current_position.set_value(current_position)
self.char_current_position.set_value(self.current_position)
if self.homekit_target is None or \ if self.homekit_target is None or \
abs(self.current_position - self.homekit_target) < 6: abs(current_position - self.homekit_target) < 6:
self.char_target_position.set_value(self.current_position) self.char_target_position.set_value(current_position)
self.char_position_state.set_value(2)
self.homekit_target = None 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)

View file

@ -7,7 +7,8 @@ from homeassistant.components.light import (
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, add_preload_service from .accessories import (
HomeAccessory, add_preload_service, debounce, setup_char)
from .const import ( from .const import (
CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
@ -24,12 +25,9 @@ class Light(HomeAccessory):
Currently supports: state, brightness, color temperature, rgb_color. 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.""" """Initialize a new Light accessory object."""
super().__init__(name, entity_id, CATEGORY_LIGHT, **kwargs) super().__init__(*args, category=CATEGORY_LIGHT)
self.hass = hass
self.entity_id = entity_id
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
CHAR_HUE: False, CHAR_SATURATION: False, CHAR_HUE: False, CHAR_SATURATION: False,
CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False} CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False}
@ -49,36 +47,29 @@ class Light(HomeAccessory):
self._saturation = None self._saturation = None
serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars)
self.char_on = serv_light.get_characteristic(CHAR_ON) self.char_on = setup_char(
self.char_on.setter_callback = self.set_state CHAR_ON, serv_light, value=self._state, callback=self.set_state)
self.char_on.value = self._state
if CHAR_BRIGHTNESS in self.chars: if CHAR_BRIGHTNESS in self.chars:
self.char_brightness = serv_light \ self.char_brightness = setup_char(
.get_characteristic(CHAR_BRIGHTNESS) CHAR_BRIGHTNESS, serv_light, value=0,
self.char_brightness.setter_callback = self.set_brightness callback=self.set_brightness)
self.char_brightness.value = 0
if CHAR_COLOR_TEMPERATURE in self.chars: 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) \ min_mireds = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_MIN_MIREDS, 153) .attributes.get(ATTR_MIN_MIREDS, 153)
max_mireds = self.hass.states.get(self.entity_id) \ max_mireds = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_MAX_MIREDS, 500) .attributes.get(ATTR_MAX_MIREDS, 500)
self.char_color_temperature.override_properties({ self.char_color_temperature = setup_char(
'minValue': min_mireds, 'maxValue': max_mireds}) CHAR_COLOR_TEMPERATURE, serv_light, value=min_mireds,
self.char_color_temperature.value = min_mireds properties={'minValue': min_mireds, 'maxValue': max_mireds},
callback=self.set_color_temperature)
if CHAR_HUE in self.chars: if CHAR_HUE in self.chars:
self.char_hue = serv_light.get_characteristic(CHAR_HUE) self.char_hue = setup_char(
self.char_hue.setter_callback = self.set_hue CHAR_HUE, serv_light, value=0, callback=self.set_hue)
self.char_hue.value = 0
if CHAR_SATURATION in self.chars: if CHAR_SATURATION in self.chars:
self.char_saturation = serv_light \ self.char_saturation = setup_char(
.get_characteristic(CHAR_SATURATION) CHAR_SATURATION, serv_light, value=75,
self.char_saturation.setter_callback = self.set_saturation callback=self.set_saturation)
self.char_saturation.value = 75
def set_state(self, value): def set_state(self, value):
"""Set state if call came from HomeKit.""" """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) _LOGGER.debug('%s: Set state to %d', self.entity_id, value)
self._flag[CHAR_ON] = True self._flag[CHAR_ON] = True
self.char_on.set_value(value, should_callback=False)
if value == 1: if value == 1:
self.hass.components.light.turn_on(self.entity_id) self.hass.components.light.turn_on(self.entity_id)
elif value == 0: elif value == 0:
self.hass.components.light.turn_off(self.entity_id) self.hass.components.light.turn_off(self.entity_id)
@debounce
def set_brightness(self, value): def set_brightness(self, value):
"""Set brightness if call came from HomeKit.""" """Set brightness if call came from HomeKit."""
_LOGGER.debug('%s: Set brightness to %d', self.entity_id, value) _LOGGER.debug('%s: Set brightness to %d', self.entity_id, value)
self._flag[CHAR_BRIGHTNESS] = True self._flag[CHAR_BRIGHTNESS] = True
self.char_brightness.set_value(value, should_callback=False)
if value != 0: if value != 0:
self.hass.components.light.turn_on( self.hass.components.light.turn_on(
self.entity_id, brightness_pct=value) self.entity_id, brightness_pct=value)
@ -109,14 +99,12 @@ class Light(HomeAccessory):
"""Set color temperature if call came from HomeKit.""" """Set color temperature if call came from HomeKit."""
_LOGGER.debug('%s: Set color temp to %s', self.entity_id, value) _LOGGER.debug('%s: Set color temp to %s', self.entity_id, value)
self._flag[CHAR_COLOR_TEMPERATURE] = True 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) self.hass.components.light.turn_on(self.entity_id, color_temp=value)
def set_saturation(self, value): def set_saturation(self, value):
"""Set saturation if call came from HomeKit.""" """Set saturation if call came from HomeKit."""
_LOGGER.debug('%s: Set saturation to %d', self.entity_id, value) _LOGGER.debug('%s: Set saturation to %d', self.entity_id, value)
self._flag[CHAR_SATURATION] = True self._flag[CHAR_SATURATION] = True
self.char_saturation.set_value(value, should_callback=False)
self._saturation = value self._saturation = value
self.set_color() self.set_color()
@ -124,7 +112,6 @@ class Light(HomeAccessory):
"""Set hue if call came from HomeKit.""" """Set hue if call came from HomeKit."""
_LOGGER.debug('%s: Set hue to %d', self.entity_id, value) _LOGGER.debug('%s: Set hue to %d', self.entity_id, value)
self._flag[CHAR_HUE] = True self._flag[CHAR_HUE] = True
self.char_hue.set_value(value, should_callback=False)
self._hue = value self._hue = value
self.set_color() self.set_color()
@ -140,17 +127,14 @@ class Light(HomeAccessory):
self.hass.components.light.turn_on( self.hass.components.light.turn_on(
self.entity_id, hs_color=color) 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.""" """Update light after state change."""
if not new_state:
return
# Handle State # Handle State
state = new_state.state state = new_state.state
if state in (STATE_ON, STATE_OFF): if state in (STATE_ON, STATE_OFF):
self._state = 1 if state == STATE_ON else 0 self._state = 1 if state == STATE_ON else 0
if not self._flag[CHAR_ON] and self.char_on.value != self._state: 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 self._flag[CHAR_ON] = False
# Handle Brightness # Handle Brightness
@ -159,17 +143,16 @@ class Light(HomeAccessory):
if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int): if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int):
brightness = round(brightness / 255 * 100, 0) brightness = round(brightness / 255 * 100, 0)
if self.char_brightness.value != brightness: if self.char_brightness.value != brightness:
self.char_brightness.set_value(brightness, self.char_brightness.set_value(brightness)
should_callback=False)
self._flag[CHAR_BRIGHTNESS] = False self._flag[CHAR_BRIGHTNESS] = False
# Handle color temperature # Handle color temperature
if CHAR_COLOR_TEMPERATURE in self.chars: if CHAR_COLOR_TEMPERATURE in self.chars:
color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP) color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP)
if not self._flag[CHAR_COLOR_TEMPERATURE] \ if not self._flag[CHAR_COLOR_TEMPERATURE] \
and isinstance(color_temperature, int): and isinstance(color_temperature, int) and \
self.char_color_temperature.set_value(color_temperature, self.char_color_temperature.value != color_temperature:
should_callback=False) self.char_color_temperature.set_value(color_temperature)
self._flag[CHAR_COLOR_TEMPERATURE] = False self._flag[CHAR_COLOR_TEMPERATURE] = False
# Handle Color # Handle Color
@ -180,8 +163,7 @@ class Light(HomeAccessory):
hue != self._hue or saturation != self._saturation) and \ hue != self._hue or saturation != self._saturation) and \
isinstance(hue, (int, float)) and \ isinstance(hue, (int, float)) and \
isinstance(saturation, (int, float)): isinstance(saturation, (int, float)):
self.char_hue.set_value(hue, should_callback=False) self.char_hue.set_value(hue)
self.char_saturation.set_value(saturation, self.char_saturation.set_value(saturation)
should_callback=False)
self._hue, self._saturation = (hue, saturation) self._hue, self._saturation = (hue, saturation)
self._flag[RGB_COLOR] = False self._flag[RGB_COLOR] = False

View 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

View file

@ -7,7 +7,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_CODE) ATTR_ENTITY_ID, ATTR_CODE)
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, add_preload_service from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import ( from .const import (
CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM, CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM,
CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE) CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE)
@ -27,33 +27,24 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
class SecuritySystem(HomeAccessory): class SecuritySystem(HomeAccessory):
"""Generate an SecuritySystem accessory for an alarm control panel.""" """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.""" """Initialize a SecuritySystem accessory object."""
super().__init__(display_name, entity_id, super().__init__(*args, category=CATEGORY_ALARM_SYSTEM)
CATEGORY_ALARM_SYSTEM, **kwargs) self._alarm_code = config[ATTR_CODE]
self.hass = hass
self.entity_id = entity_id
self._alarm_code = alarm_code
self.flag_target_state = False self.flag_target_state = False
serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
self.char_current_state = serv_alarm. \ self.char_current_state = setup_char(
get_characteristic(CHAR_CURRENT_SECURITY_STATE) CHAR_CURRENT_SECURITY_STATE, serv_alarm, value=3)
self.char_current_state.value = 3 self.char_target_state = setup_char(
self.char_target_state = serv_alarm. \ CHAR_TARGET_SECURITY_STATE, serv_alarm, value=3,
get_characteristic(CHAR_TARGET_SECURITY_STATE) callback=self.set_security_state)
self.char_target_state.value = 3
self.char_target_state.setter_callback = self.set_security_state
def set_security_state(self, value): def set_security_state(self, value):
"""Move security state to value if call came from HomeKit.""" """Move security state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set security state to %d', _LOGGER.debug('%s: Set security state to %d',
self.entity_id, value) self.entity_id, value)
self.flag_target_state = True self.flag_target_state = True
self.char_target_state.set_value(value, should_callback=False)
hass_value = HOMEKIT_TO_HASS[value] hass_value = HOMEKIT_TO_HASS[value]
service = STATE_TO_SERVICE[hass_value] service = STATE_TO_SERVICE[hass_value]
@ -62,23 +53,16 @@ class SecuritySystem(HomeAccessory):
params[ATTR_CODE] = self._alarm_code params[ATTR_CODE] = self._alarm_code
self.hass.services.call('alarm_control_panel', service, params) 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.""" """Update security state after state changed."""
if new_state is None:
return
hass_state = new_state.state hass_state = new_state.state
if hass_state not in HASS_TO_HOMEKIT: if hass_state in HASS_TO_HOMEKIT:
return
current_security_state = HASS_TO_HOMEKIT[hass_state] current_security_state = HASS_TO_HOMEKIT[hass_state]
self.char_current_state.set_value(current_security_state, self.char_current_state.set_value(current_security_state)
should_callback=False)
_LOGGER.debug('%s: Updated current state to %s (%d)', _LOGGER.debug('%s: Updated current state to %s (%d)',
self.entity_id, hass_state, current_security_state) self.entity_id, hass_state, current_security_state)
if not self.flag_target_state: if not self.flag_target_state:
self.char_target_state.set_value(current_security_state, self.char_target_state.set_value(current_security_state)
should_callback=False)
if self.char_target_state.value == self.char_current_state.value: if self.char_target_state.value == self.char_current_state.value:
self.flag_target_state = False self.flag_target_state = False

View file

@ -2,18 +2,41 @@
import logging import logging
from homeassistant.const import ( 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 . import TYPES
from .accessories import HomeAccessory, add_preload_service from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import ( from .const import (
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS,
from .util import convert_to_float, temperature_to_homekit 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__) _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') @TYPES.register('TemperatureSensor')
class TemperatureSensor(HomeAccessory): class TemperatureSensor(HomeAccessory):
@ -22,29 +45,22 @@ class TemperatureSensor(HomeAccessory):
Sensor entity must return temperature in °C, °F. 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.""" """Initialize a TemperatureSensor accessory object."""
super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs) super().__init__(*args, category=CATEGORY_SENSOR)
self.hass = hass
self.entity_id = entity_id
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE) self.char_temp = setup_char(
self.char_temp.override_properties(properties=PROP_CELSIUS) CHAR_CURRENT_TEMPERATURE, serv_temp, value=0,
self.char_temp.value = 0 properties=PROP_CELSIUS)
self.unit = None 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.""" """Update temperature after state changed."""
if new_state is None:
return
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
temperature = convert_to_float(new_state.state) temperature = convert_to_float(new_state.state)
if temperature: if temperature:
temperature = temperature_to_homekit(temperature, unit) 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', _LOGGER.debug('%s: Current temperature set to %d°C',
self.entity_id, temperature) self.entity_id, temperature)
@ -53,25 +69,113 @@ class TemperatureSensor(HomeAccessory):
class HumiditySensor(HomeAccessory): class HumiditySensor(HomeAccessory):
"""Generate a HumiditySensor accessory as humidity sensor.""" """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.""" """Initialize a HumiditySensor accessory object."""
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) super().__init__(*args, category=CATEGORY_SENSOR)
self.hass = hass
self.entity_id = entity_id
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR) serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
self.char_humidity = serv_humidity \ self.char_humidity = setup_char(
.get_characteristic(CHAR_CURRENT_HUMIDITY) CHAR_CURRENT_HUMIDITY, serv_humidity, value=0)
self.char_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.""" """Update accessory after state change."""
if new_state is None:
return
humidity = convert_to_float(new_state.state) humidity = convert_to_float(new_state.state)
if humidity: if humidity:
self.char_humidity.set_value(humidity, should_callback=False) self.char_humidity.set_value(humidity)
_LOGGER.debug('%s: Percent set to %d%%', _LOGGER.debug('%s: Percent set to %d%%',
self.entity_id, humidity) 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)

View file

@ -6,7 +6,7 @@ from homeassistant.const import (
from homeassistant.core import split_entity_id from homeassistant.core import split_entity_id
from . import TYPES 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 from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -16,40 +16,30 @@ _LOGGER = logging.getLogger(__name__)
class Switch(HomeAccessory): class Switch(HomeAccessory):
"""Generate a Switch accessory.""" """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.""" """Initialize a Switch accessory object to represent a remote."""
super().__init__(display_name, entity_id, CATEGORY_SWITCH, **kwargs) super().__init__(*args, category=CATEGORY_SWITCH)
self._domain = split_entity_id(self.entity_id)[0]
self.hass = hass
self.entity_id = entity_id
self._domain = split_entity_id(entity_id)[0]
self.flag_target_state = False self.flag_target_state = False
serv_switch = add_preload_service(self, SERV_SWITCH) serv_switch = add_preload_service(self, SERV_SWITCH)
self.char_on = serv_switch.get_characteristic(CHAR_ON) self.char_on = setup_char(
self.char_on.value = False CHAR_ON, serv_switch, value=False, callback=self.set_state)
self.char_on.setter_callback = self.set_state
def set_state(self, value): def set_state(self, value):
"""Move switch state to value if call came from HomeKit.""" """Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state to %s', _LOGGER.debug('%s: Set switch state to %s',
self.entity_id, value) self.entity_id, value)
self.flag_target_state = True self.flag_target_state = True
self.char_on.set_value(value, should_callback=False)
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
self.hass.services.call(self._domain, service, self.hass.services.call(self._domain, service,
{ATTR_ENTITY_ID: self.entity_id}) {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.""" """Update switch state after state changed."""
if new_state is None:
return
current_state = (new_state.state == STATE_ON) current_state = (new_state.state == STATE_ON)
if not self.flag_target_state: if not self.flag_target_state:
_LOGGER.debug('%s: Set current state to %s', _LOGGER.debug('%s: Set current state to %s',
self.entity_id, current_state) 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 self.flag_target_state = False

View file

@ -5,12 +5,15 @@ from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE,
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, 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 ( 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 . import TYPES
from .accessories import HomeAccessory, add_preload_service from .accessories import (
HomeAccessory, add_preload_service, debounce, setup_char)
from .const import ( from .const import (
CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, 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} STATE_COOL: 2, STATE_AUTO: 3}
HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()} 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') @TYPES.register('Thermostat')
class Thermostat(HomeAccessory): class Thermostat(HomeAccessory):
"""Generate a Thermostat accessory for a climate.""" """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.""" """Initialize a Thermostat accessory object."""
super().__init__(display_name, entity_id, super().__init__(*args, category=CATEGORY_THERMOSTAT)
CATEGORY_THERMOSTAT, **kwargs)
self.hass = hass
self.entity_id = entity_id
self._call_timer = None
self._unit = TEMP_CELSIUS self._unit = TEMP_CELSIUS
self.heat_cool_flag_target_state = False self.heat_cool_flag_target_state = False
self.temperature_flag_target_state = False self.temperature_flag_target_state = False
self.coolingthresh_flag_target_state = False self.coolingthresh_flag_target_state = False
self.heatingthresh_flag_target_state = False self.heatingthresh_flag_target_state = False
# Add additional characteristics if auto mode is supported # Add additional characteristics if auto mode is supported
extra_chars = [ self.chars = []
CHAR_COOLING_THRESHOLD_TEMPERATURE, features = self.hass.states.get(self.entity_id) \
CHAR_HEATING_THRESHOLD_TEMPERATURE] if support_auto else None .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(
serv_thermostat = add_preload_service(self, SERV_THERMOSTAT, self, SERV_THERMOSTAT, self.chars)
extra_chars)
# Current and target mode characteristics # Current and target mode characteristics
self.char_current_heat_cool = serv_thermostat. \ self.char_current_heat_cool = setup_char(
get_characteristic(CHAR_CURRENT_HEATING_COOLING) CHAR_CURRENT_HEATING_COOLING, serv_thermostat, value=0)
self.char_current_heat_cool.value = 0 self.char_target_heat_cool = setup_char(
self.char_target_heat_cool = serv_thermostat. \ CHAR_TARGET_HEATING_COOLING, serv_thermostat, value=0,
get_characteristic(CHAR_TARGET_HEATING_COOLING) callback=self.set_heat_cool)
self.char_target_heat_cool.value = 0
self.char_target_heat_cool.setter_callback = self.set_heat_cool
# Current and target temperature characteristics # Current and target temperature characteristics
self.char_current_temp = serv_thermostat. \ self.char_current_temp = setup_char(
get_characteristic(CHAR_CURRENT_TEMPERATURE) CHAR_CURRENT_TEMPERATURE, serv_thermostat, value=21.0)
self.char_current_temp.value = 21.0 self.char_target_temp = setup_char(
self.char_target_temp = serv_thermostat. \ CHAR_TARGET_TEMPERATURE, serv_thermostat, value=21.0,
get_characteristic(CHAR_TARGET_TEMPERATURE) callback=self.set_target_temperature)
self.char_target_temp.value = 21.0
self.char_target_temp.setter_callback = self.set_target_temperature
# Display units characteristic # Display units characteristic
self.char_display_units = serv_thermostat. \ self.char_display_units = setup_char(
get_characteristic(CHAR_TEMP_DISPLAY_UNITS) CHAR_TEMP_DISPLAY_UNITS, serv_thermostat, value=0)
self.char_display_units.value = 0
# If the device supports it: high and low temperature characteristics # 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_cooling_thresh_temp = None
self.char_heating_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): def set_heat_cool(self, value):
"""Move operation mode to value if call came from HomeKit.""" """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: if value in HC_HOMEKIT_TO_HASS:
_LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
self.heat_cool_flag_target_state = True self.heat_cool_flag_target_state = True
@ -105,12 +96,12 @@ class Thermostat(HomeAccessory):
self.hass.components.climate.set_operation_mode( self.hass.components.climate.set_operation_mode(
operation_mode=hass_value, entity_id=self.entity_id) operation_mode=hass_value, entity_id=self.entity_id)
@debounce
def set_cooling_threshold(self, value): def set_cooling_threshold(self, value):
"""Set cooling threshold temp to value if call came from HomeKit.""" """Set cooling threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C', _LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C',
self.entity_id, value) self.entity_id, value)
self.coolingthresh_flag_target_state = True 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 = self.char_heating_thresh_temp.value
low = temperature_to_states(low, self._unit) low = temperature_to_states(low, self._unit)
value = temperature_to_states(value, 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, entity_id=self.entity_id, target_temp_high=value,
target_temp_low=low) target_temp_low=low)
@debounce
def set_heating_threshold(self, value): def set_heating_threshold(self, value):
"""Set heating threshold temp to value if call came from HomeKit.""" """Set heating threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
self.entity_id, value) self.entity_id, value)
self.heatingthresh_flag_target_state = True 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 # Home assistant always wants to set low and high at the same time
high = self.char_cooling_thresh_temp.value high = self.char_cooling_thresh_temp.value
high = temperature_to_states(high, self._unit) high = temperature_to_states(high, self._unit)
@ -132,21 +123,18 @@ class Thermostat(HomeAccessory):
entity_id=self.entity_id, target_temp_high=high, entity_id=self.entity_id, target_temp_high=high,
target_temp_low=value) target_temp_low=value)
@debounce
def set_target_temperature(self, value): def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit.""" """Set target temperature to value if call came from HomeKit."""
_LOGGER.debug('%s: Set target temperature to %.2f°C', _LOGGER.debug('%s: Set target temperature to %.2f°C',
self.entity_id, value) self.entity_id, value)
self.temperature_flag_target_state = True self.temperature_flag_target_state = True
self.char_target_temp.set_value(value, should_callback=False)
value = temperature_to_states(value, self._unit) value = temperature_to_states(value, self._unit)
self.hass.components.climate.set_temperature( self.hass.components.climate.set_temperature(
temperature=value, entity_id=self.entity_id) 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.""" """Update security state after state changed."""
if new_state is None:
return
self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT,
TEMP_CELSIUS) TEMP_CELSIUS)
@ -161,8 +149,7 @@ class Thermostat(HomeAccessory):
if isinstance(target_temp, (int, float)): if isinstance(target_temp, (int, float)):
target_temp = temperature_to_homekit(target_temp, self._unit) target_temp = temperature_to_homekit(target_temp, self._unit)
if not self.temperature_flag_target_state: if not self.temperature_flag_target_state:
self.char_target_temp.set_value(target_temp, self.char_target_temp.set_value(target_temp)
should_callback=False)
self.temperature_flag_target_state = False self.temperature_flag_target_state = False
# Update cooling threshold temperature if characteristic exists # Update cooling threshold temperature if characteristic exists
@ -172,8 +159,7 @@ class Thermostat(HomeAccessory):
cooling_thresh = temperature_to_homekit(cooling_thresh, cooling_thresh = temperature_to_homekit(cooling_thresh,
self._unit) self._unit)
if not self.coolingthresh_flag_target_state: if not self.coolingthresh_flag_target_state:
self.char_cooling_thresh_temp.set_value( self.char_cooling_thresh_temp.set_value(cooling_thresh)
cooling_thresh, should_callback=False)
self.coolingthresh_flag_target_state = False self.coolingthresh_flag_target_state = False
# Update heating threshold temperature if characteristic exists # Update heating threshold temperature if characteristic exists
@ -183,8 +169,7 @@ class Thermostat(HomeAccessory):
heating_thresh = temperature_to_homekit(heating_thresh, heating_thresh = temperature_to_homekit(heating_thresh,
self._unit) self._unit)
if not self.heatingthresh_flag_target_state: if not self.heatingthresh_flag_target_state:
self.char_heating_thresh_temp.set_value( self.char_heating_thresh_temp.set_value(heating_thresh)
heating_thresh, should_callback=False)
self.heatingthresh_flag_target_state = False self.heatingthresh_flag_target_state = False
# Update display units # Update display units
@ -197,7 +182,7 @@ class Thermostat(HomeAccessory):
and operation_mode in HC_HASS_TO_HOMEKIT: and operation_mode in HC_HASS_TO_HOMEKIT:
if not self.heat_cool_flag_target_state: if not self.heat_cool_flag_target_state:
self.char_target_heat_cool.set_value( 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 self.heat_cool_flag_target_state = False
# Set current operation mode based on temperatures and target mode # Set current operation mode based on temperatures and target mode

View file

@ -33,7 +33,7 @@ def validate_entity_config(values):
return entities return entities
def show_setup_message(bridge, hass): def show_setup_message(hass, bridge):
"""Display persistent notification with setup information.""" """Display persistent notification with setup information."""
pin = bridge.pincode.decode() pin = bridge.pincode.decode()
_LOGGER.info('Pincode: %s', pin) _LOGGER.info('Pincode: %s', pin)
@ -64,3 +64,16 @@ def temperature_to_homekit(temperature, unit):
def temperature_to_states(temperature, unit): def temperature_to_states(temperature, unit):
"""Convert temperature back from Celsius to Home Assistant unit.""" """Convert temperature back from Celsius to Home Assistant unit."""
return round(temp_util.convert(temperature, TEMP_CELSIUS, unit), 1) 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

View 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

View file

@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['pyhomematic==0.1.40'] REQUIREMENTS = ['pyhomematic==0.1.41']
DOMAIN = 'homematic' DOMAIN = 'homematic'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -69,7 +69,8 @@ HM_DEVICE_TYPES = {
'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor', 'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor',
'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch', 'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch',
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat'], 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat',
'IPWeatherSensor'],
DISCOVER_CLIMATE: [ DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
@ -78,7 +79,7 @@ HM_DEVICE_TYPES = {
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2', 'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
'WiredSensor', 'PresenceIP'], 'WiredSensor', 'PresenceIP', 'IPWeatherSensor'],
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'], DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'],
DISCOVER_LOCKS: ['KeyMatic'] DISCOVER_LOCKS: ['KeyMatic']
} }
@ -89,7 +90,7 @@ HM_IGNORE_DISCOVERY_NODE = [
] ]
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = {
'ACTUAL_TEMPERATURE': ['IPAreaThermostat'], 'ACTUAL_TEMPERATURE': ['IPAreaThermostat', 'IPWeatherSensor'],
} }
HM_ATTRIBUTE_SUPPORT = { HM_ATTRIBUTE_SUPPORT = {

View file

@ -131,3 +131,9 @@ async def async_setup_entry(hass, entry):
bridge = HueBridge(hass, entry, allow_unreachable, allow_groups) bridge = HueBridge(hass, entry, allow_unreachable, allow_groups)
hass.data[DOMAIN][host] = bridge hass.data[DOMAIN][host] = bridge
return await bridge.async_setup() 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()

View file

@ -30,6 +30,7 @@ class HueBridge(object):
self.allow_groups = allow_groups self.allow_groups = allow_groups
self.available = True self.available = True
self.api = None self.api = None
self._cancel_retry_setup = None
@property @property
def host(self): def host(self):
@ -39,18 +40,17 @@ class HueBridge(object):
async def async_setup(self, tries=0): async def async_setup(self, tries=0):
"""Set up a phue bridge based on host parameter.""" """Set up a phue bridge based on host parameter."""
host = self.host host = self.host
hass = self.hass
try: try:
self.api = await get_bridge( self.api = await get_bridge(
self.hass, host, hass, host, self.config_entry.data['username'])
self.config_entry.data['username']
)
except AuthenticationRequired: except AuthenticationRequired:
# usernames can become invalid if hub is reset or user removed. # usernames can become invalid if hub is reset or user removed.
# We are going to fail the config entry setup and initiate a new # We are going to fail the config entry setup and initiate a new
# linking procedure. When linking succeeds, it will remove the # linking procedure. When linking succeeds, it will remove the
# old config entry. # 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={ DOMAIN, source='import', data={
'host': host, 'host': host,
} }
@ -68,8 +68,8 @@ class HueBridge(object):
# This feels hacky, we should find a better way to do this # This feels hacky, we should find a better way to do this
self.config_entry.state = config_entries.ENTRY_STATE_LOADED self.config_entry.state = config_entries.ENTRY_STATE_LOADED
# Unhandled edge case: cancel this if we discover bridge on new IP self._cancel_retry_setup = hass.helpers.event.async_call_later(
self.hass.helpers.event.async_call_later(retry_delay, retry_setup) retry_delay, retry_setup)
return False return False
@ -78,16 +78,43 @@ class HueBridge(object):
host) host)
return False return False
self.hass.async_add_job( hass.async_add_job(hass.config_entries.async_forward_entry_setup(
self.hass.helpers.discovery.async_load_platform( self.config_entry, 'light'))
'light', DOMAIN, {'host': host}))
self.hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
schema=SCENE_SCHEMA) schema=SCENE_SCHEMA)
return True 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): async def hue_activate_scene(self, call, updated=False):
"""Service to call directly into bridge to set scenes.""" """Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME] group_name = call.data[ATTR_GROUP_NAME]

View file

@ -6,7 +6,7 @@ import os
import async_timeout import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries, data_entry_flow
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
@ -41,7 +41,7 @@ def _find_username_from_config(hass, filename):
@config_entries.HANDLERS.register(DOMAIN) @config_entries.HANDLERS.register(DOMAIN)
class HueFlowHandler(config_entries.ConfigFlowHandler): class HueFlowHandler(data_entry_flow.FlowHandler):
"""Handle a Hue config flow.""" """Handle a Hue config flow."""
VERSION = 1 VERSION = 1

View file

@ -1,4 +1,5 @@
"""IHC component. """
Support for IHC devices.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ihc/ https://home-assistant.io/components/ihc/
@ -6,18 +7,18 @@ https://home-assistant.io/components/ihc/
import logging import logging
import os.path import os.path
import xml.etree.ElementTree import xml.etree.ElementTree
import voluptuous as vol import voluptuous as vol
from homeassistant.components.ihc.const import ( from homeassistant.components.ihc.const import (
ATTR_IHC_ID, ATTR_VALUE, CONF_INFO, CONF_AUTOSETUP, ATTR_IHC_ID, ATTR_VALUE, CONF_AUTOSETUP, CONF_BINARY_SENSOR, CONF_DIMMABLE,
CONF_BINARY_SENSOR, CONF_LIGHT, CONF_SENSOR, CONF_SWITCH, CONF_INFO, CONF_INVERTING, CONF_LIGHT, CONF_NODE, CONF_SENSOR, CONF_SWITCH,
CONF_XPATH, CONF_NODE, CONF_DIMMABLE, CONF_INVERTING, CONF_XPATH, SERVICE_SET_RUNTIME_VALUE_BOOL,
SERVICE_SET_RUNTIME_VALUE_BOOL, SERVICE_SET_RUNTIME_VALUE_INT, SERVICE_SET_RUNTIME_VALUE_FLOAT, SERVICE_SET_RUNTIME_VALUE_INT)
SERVICE_SET_RUNTIME_VALUE_FLOAT)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.const import ( from homeassistant.const import (
CONF_URL, CONF_USERNAME, CONF_PASSWORD, CONF_ID, CONF_NAME, CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT,
CONF_UNIT_OF_MEASUREMENT, CONF_TYPE, TEMP_CELSIUS) CONF_URL, CONF_USERNAME, TEMP_CELSIUS)
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
@ -36,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_AUTOSETUP, default=True): cv.boolean, 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) }, extra=vol.ALLOW_EXTRA)
@ -106,7 +107,7 @@ def setup(hass, config):
ihc_controller = IHCController(url, username, password) ihc_controller = IHCController(url, username, password)
if not ihc_controller.authenticate(): if not ihc_controller.authenticate():
_LOGGER.error("Unable to authenticate on ihc controller.") _LOGGER.error("Unable to authenticate on IHC controller")
return False return False
if (conf[CONF_AUTOSETUP] and 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.""" """Auto setup of IHC products from the ihc project file."""
project_xml = ihc_controller.get_project() project_xml = ihc_controller.get_project()
if not project_xml: if not project_xml:
_LOGGER.error("Unable to read project from ihc controller.") _LOGGER.error("Unable to read project from ICH controller")
return False return False
project = xml.etree.ElementTree.fromstring(project_xml) 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): def get_discovery_info(component_setup, groups):
"""Get discovery info for specified component.""" """Get discovery info for specified IHC component."""
discovery_data = {} discovery_data = {}
for group in groups: for group in groups:
groupname = group.attrib['name'] groupname = group.attrib['name']
@ -173,7 +174,7 @@ def get_discovery_info(component_setup, groups):
def setup_service_functions(hass: HomeAssistantType, ihc_controller): def setup_service_functions(hass: HomeAssistantType, ihc_controller):
"""Setup the ihc service functions.""" """Setup the IHC service functions."""
def set_runtime_value_bool(call): def set_runtime_value_bool(call):
"""Set a IHC runtime bool value service function.""" """Set a IHC runtime bool value service function."""
ihc_id = call.data[ATTR_IHC_ID] ihc_id = call.data[ATTR_IHC_ID]

View file

@ -1,4 +1,4 @@
"""Implements a base class for all IHC devices.""" """Implementation of a base class for all IHC devices."""
import asyncio import asyncio
from xml.etree.ElementTree import Element from xml.etree.ElementTree import Element
@ -6,7 +6,7 @@ from homeassistant.helpers.entity import Entity
class IHCDevice(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 All IHC devices have an associated IHC resource. IHCDevice handled the
registration of the IHC controller callback when the IHC resource changes. registration of the IHC controller callback when the IHC resource changes.
@ -31,13 +31,13 @@ class IHCDevice(Entity):
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Add callback for ihc changes.""" """Add callback for IHC changes."""
self.ihc_controller.add_notify_event( self.ihc_controller.add_notify_event(
self.ihc_id, self.on_ihc_change, True) self.ihc_id, self.on_ihc_change, True)
@property @property
def should_poll(self) -> bool: def should_poll(self) -> bool:
"""No polling needed for ihc devices.""" """No polling needed for IHC devices."""
return False return False
@property @property
@ -58,7 +58,7 @@ class IHCDevice(Entity):
} }
def on_ihc_change(self, ihc_id, value): 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. Derived classes must overwrite this to do device specific stuff.
""" """

View file

@ -334,7 +334,7 @@ class SetIntentHandler(intent.IntentHandler):
async def async_setup(hass, config): async def async_setup(hass, config):
"""Expose light control via state machine and services.""" """Expose light control via state machine and services."""
component = EntityComponent( component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS) _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS)
await component.async_setup(config) await component.async_setup(config)
@ -388,6 +388,16 @@ async def async_setup(hass, config):
return True 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: class Profiles:
"""Representation of available color profiles.""" """Representation of available color profiles."""

View file

@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.abode/ https://home-assistant.io/components/light.abode/
""" """
import logging import logging
from math import ceil
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_BRIGHTNESS, ATTR_HS_COLOR,
@ -51,7 +51,9 @@ class AbodeLight(AbodeDevice, Light):
*kwargs[ATTR_HS_COLOR])) *kwargs[ATTR_HS_COLOR]))
if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable: 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: else:
self._device.switch_on() self._device.switch_on()
@ -68,7 +70,12 @@ class AbodeLight(AbodeDevice, Light):
def brightness(self): def brightness(self):
"""Return the brightness of the light.""" """Return the brightness of the light."""
if self._device.is_dimmable and self._device.has_brightness: 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 @property
def hs_color(self): def hs_color(self):

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

View file

@ -34,6 +34,7 @@ class HiveDeviceLight(Light):
self.device_type = hivedevice["HA_DeviceType"] self.device_type = hivedevice["HA_DeviceType"]
self.light_device_type = hivedevice["Hive_Light_DeviceType"] self.light_device_type = hivedevice["Hive_Light_DeviceType"]
self.session = hivesession self.session = hivesession
self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type, self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id) self.node_id)
self.session.entities.append(self) self.session.entities.append(self)
@ -48,6 +49,11 @@ class HiveDeviceLight(Light):
"""Return the display name of this light.""" """Return the display name of this light."""
return self.node_name return self.node_name
@property
def device_state_attributes(self):
"""Show Device Attributes."""
return self.attributes
@property @property
def brightness(self): def brightness(self):
"""Brightness of the light (an integer in the range 1-255).""" """Brightness of the light (an integer in the range 1-255)."""
@ -136,3 +142,5 @@ class HiveDeviceLight(Light):
def update(self): def update(self):
"""Update all Node data from Hive.""" """Update all Node data from Hive."""
self.session.core.update_data(self.node_id) self.session.core.update_data(self.node_id)
self.attributes = self.session.attributes.state_attributes(
self.node_id)

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

View file

@ -49,11 +49,17 @@ GROUP_MIN_API_VERSION = (1, 13, 0)
async def async_setup_platform(hass, config, async_add_devices, async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None): discovery_info=None):
"""Set up the Hue lights.""" """Old way of setting up Hue lights.
if discovery_info is None:
return
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_lights = {}
cur_groups = {} cur_groups = {}

View file

@ -1,11 +1,6 @@
""" """
Support for Nanoleaf Aurora platform. 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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.nanoleaf_aurora/ https://home-assistant.io/components/light.nanoleaf_aurora/
""" """
@ -15,9 +10,9 @@ import voluptuous as vol
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR,
SUPPORT_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP,
SUPPORT_COLOR, PLATFORM_SCHEMA, Light) SUPPORT_EFFECT, Light)
from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_NAME from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util import color as color_util from homeassistant.util import color as color_util
from homeassistant.util.color import \ from homeassistant.util.color import \
@ -25,20 +20,24 @@ from homeassistant.util.color import \
REQUIREMENTS = ['nanoleaf==0.4.1'] 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_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
SUPPORT_COLOR) SUPPORT_COLOR)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): 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): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Nanoleaf Aurora device.""" """Set up the Nanoleaf Aurora device."""
import nanoleaf import nanoleaf
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
@ -47,8 +46,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
aurora_light.hass_name = name aurora_light.hass_name = name
if aurora_light.on is None: if aurora_light.on is None:
_LOGGER.error("Could not connect to \ _LOGGER.error(
Nanoleaf Aurora: %s on %s", name, host) "Could not connect to Nanoleaf Aurora: %s on %s", name, host)
return
add_devices([AuroraLight(aurora_light)], True) add_devices([AuroraLight(aurora_light)], True)
@ -56,7 +57,7 @@ class AuroraLight(Light):
"""Representation of a Nanoleaf Aurora.""" """Representation of a Nanoleaf Aurora."""
def __init__(self, light): def __init__(self, light):
"""Initialize an Aurora.""" """Initialize an Aurora light."""
self._brightness = None self._brightness = None
self._color_temp = None self._color_temp = None
self._effect = None self._effect = None
@ -99,7 +100,7 @@ class AuroraLight(Light):
@property @property
def icon(self): def icon(self):
"""Return the icon to use in the frontend, if any.""" """Return the icon to use in the frontend, if any."""
return "mdi:triangle-outline" return ICON
@property @property
def is_on(self): def is_on(self):
@ -141,10 +142,7 @@ class AuroraLight(Light):
self._light.on = False self._light.on = False
def update(self): def update(self):
"""Fetch new state data for this light. """Fetch new state data for this light."""
This is the only method that should fetch new data for Home Assistant.
"""
self._brightness = self._light.brightness self._brightness = self._light.brightness
self._color_temp = self._light.color_temperature self._color_temp = self._light.color_temperature
self._effect = self._light.effect self._effect = self._light.effect

View file

@ -27,9 +27,9 @@ class QSLight(QSToggleEntity, Light):
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light (0-255).""" """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 @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_BRIGHTNESS if self._dim else 0 return SUPPORT_BRIGHTNESS if self.device.is_dimmer else 0

View file

@ -24,6 +24,14 @@ REQUIREMENTS = ['yeelight==0.4.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
LEGACY_DEVICE_TYPE_MAP = {
'color1': 'rgb',
'mono1': 'white',
'strip1': 'strip',
'bslamp1': 'bedside',
'ceiling1': 'ceiling',
}
CONF_TRANSITION = 'transition' CONF_TRANSITION = 'transition'
DEFAULT_TRANSITION = 350 DEFAULT_TRANSITION = 350
@ -122,8 +130,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is not None: if discovery_info is not None:
_LOGGER.debug("Adding autodetected %s", discovery_info['hostname']) _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. # 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']) discovery_info['properties']['mac'])
device = {'name': name, 'ipaddr': discovery_info['host']} device = {'name': name, 'ipaddr': discovery_info['host']}

View file

@ -38,6 +38,7 @@ class BMWLock(LockDevice):
self._vehicle = vehicle self._vehicle = vehicle
self._attribute = attribute self._attribute = attribute
self._name = '{} {}'.format(self._vehicle.name, self._attribute) self._name = '{} {}'.format(self._vehicle.name, self._attribute)
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
self._sensor_name = sensor_name self._sensor_name = sensor_name
self._state = None self._state = None
@ -49,6 +50,11 @@ class BMWLock(LockDevice):
""" """
return False return False
@property
def unique_id(self):
"""Return the unique ID of the lock."""
return self._unique_id
@property @property
def name(self): def name(self):
"""Return the name of the lock.""" """Return the name of the lock."""

View file

@ -14,7 +14,7 @@ from homeassistant.const import CONF_HOST
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['pylutron-caseta==0.3.0'] REQUIREMENTS = ['pylutron-caseta==0.5.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -22,12 +22,22 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 62910 DEFAULT_PORT = 62910
DOMAIN = 'maxcube' 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({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_GATEWAYS, default={}):
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.All(cv.ensure_list, [CONFIG_GATEWAY])
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -36,18 +46,30 @@ def setup(hass, config):
"""Establish connection to MAX! Cube.""" """Establish connection to MAX! Cube."""
from maxcube.connection import MaxCubeConnection from maxcube.connection import MaxCubeConnection
from maxcube.cube import MaxCube from maxcube.cube import MaxCube
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {}
host = config.get(DOMAIN).get(CONF_HOST) connection_failed = 0
port = config.get(DOMAIN).get(CONF_PORT) gateways = config[DOMAIN][CONF_GATEWAYS]
for gateway in gateways:
host = gateway[CONF_HOST]
port = gateway[CONF_PORT]
try: try:
cube = MaxCube(MaxCubeConnection(host, port)) cube = MaxCube(MaxCubeConnection(host, port))
except timeout: hass.data[DATA_KEY][host] = MaxCubeHandle(cube)
_LOGGER.error("Connection to Max!Cube could not be established") except timeout as ex:
cube = None _LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex))
return False 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
hass.data[MAXCUBE_HANDLE] = MaxCubeHandle(cube) if connection_failed >= len(gateways):
return False
load_platform(hass, 'climate', DOMAIN) load_platform(hass, 'climate', DOMAIN)
load_platform(hass, 'binary_sensor', DOMAIN) load_platform(hass, 'binary_sensor', DOMAIN)

View file

@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA) SERVICE_PLAY_MEDIA)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['youtube_dl==2018.04.03'] REQUIREMENTS = ['youtube_dl==2018.04.16']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

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

View file

@ -37,30 +37,30 @@ REQUIREMENTS = ['xmltodict==0.11.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
STATE_GROUPED = 'grouped'
ATTR_MASTER = 'master' 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' DATA_BLUESOUND = 'bluesound'
DEFAULT_PORT = 11000 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_OFFLINE_CHECK_TIMEOUT = 180
NODE_RETRY_INITIATION = timedelta(minutes=3) 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({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [{ vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [{
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_NAME): cv.string, 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) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_player)
async def async_setup_platform(hass, config, async_add_devices, async def async_setup_platform(
discovery_info=None): hass, config, async_add_devices, discovery_info=None):
"""Set up the Bluesound platforms.""" """Set up the Bluesound platforms."""
if DATA_BLUESOUND not in hass.data: if DATA_BLUESOUND not in hass.data:
hass.data[DATA_BLUESOUND] = [] hass.data[DATA_BLUESOUND] = []
@ -202,6 +202,9 @@ class BluesoundPlayer(MediaPlayerDevice):
if self.port is None: if self.port is None:
self.port = DEFAULT_PORT self.port = DEFAULT_PORT
class _TimeoutException(Exception):
pass
@staticmethod @staticmethod
def _try_get_index(string, search_string): def _try_get_index(string, search_string):
"""Get the index.""" """Get the index."""
@ -258,7 +261,8 @@ class BluesoundPlayer(MediaPlayerDevice):
while True: while True:
await self.async_update_status() 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) _LOGGER.info("Node %s is offline, retrying later", self._name)
await asyncio.sleep( await asyncio.sleep(
NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop) NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop)
@ -293,8 +297,8 @@ class BluesoundPlayer(MediaPlayerDevice):
self._retry_remove = async_track_time_interval( self._retry_remove = async_track_time_interval(
self._hass, self.async_init, NODE_RETRY_INITIATION) self._hass, self.async_init, NODE_RETRY_INITIATION)
except Exception: except Exception:
_LOGGER.exception("Unexpected when initiating error in %s", _LOGGER.exception(
self.host) "Unexpected when initiating error in %s", self.host)
raise raise
async def async_update(self): async def async_update(self):
@ -307,8 +311,8 @@ class BluesoundPlayer(MediaPlayerDevice):
await self.async_update_captures() await self.async_update_captures()
await self.async_update_services() await self.async_update_services()
async def send_bluesound_command(self, method, raise_timeout=False, async def send_bluesound_command(
allow_offline=False): self, method, raise_timeout=False, allow_offline=False):
"""Send command to the player.""" """Send command to the player."""
import xmltodict import xmltodict
@ -321,6 +325,7 @@ class BluesoundPlayer(MediaPlayerDevice):
_LOGGER.debug("Calling URL: %s", url) _LOGGER.debug("Calling URL: %s", url)
response = None response = None
try: try:
websession = async_get_clientsession(self._hass) websession = async_get_clientsession(self._hass)
with async_timeout.timeout(10, loop=self._hass.loop): with async_timeout.timeout(10, loop=self._hass.loop):
@ -332,6 +337,9 @@ class BluesoundPlayer(MediaPlayerDevice):
data = None data = None
else: else:
data = xmltodict.parse(result) data = xmltodict.parse(result)
elif response.status == 595:
_LOGGER.info("Status 595 returned, treating as timeout")
raise BluesoundPlayer._TimeoutException()
else: else:
_LOGGER.error("Error %s on %s", response.status, url) _LOGGER.error("Error %s on %s", response.status, url)
return None return None
@ -366,13 +374,9 @@ class BluesoundPlayer(MediaPlayerDevice):
with async_timeout.timeout(125, loop=self._hass.loop): with async_timeout.timeout(125, loop=self._hass.loop):
response = await self._polling_session.get( response = await self._polling_session.get(
url, url, headers={CONNECTION: KEEP_ALIVE})
headers={CONNECTION: KEEP_ALIVE})
if response.status != 200: if response.status == 200:
_LOGGER.error("Error %s on %s. Trying one more time.",
response.status, url)
else:
result = await response.text() result = await response.text()
self._is_online = True self._is_online = True
self._last_status_update = dt_util.utcnow() self._last_status_update = dt_util.utcnow()
@ -380,8 +384,8 @@ class BluesoundPlayer(MediaPlayerDevice):
group_name = self._status.get('groupName', None) group_name = self._status.get('groupName', None)
if group_name != self._group_name: if group_name != self._group_name:
_LOGGER.debug('Group name change detected on device: %s', _LOGGER.debug(
self.host) "Group name change detected on device: %s", self.host)
self._group_name = group_name self._group_name = group_name
# the sleep is needed to make sure that the # the sleep is needed to make sure that the
# devices is synced # devices is synced
@ -398,14 +402,20 @@ class BluesoundPlayer(MediaPlayerDevice):
await self.force_update_sync_status() await self.force_update_sync_status()
self.async_schedule_update_ha_state() 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): except (asyncio.TimeoutError, ClientError):
self._is_online = False self._is_online = False
self._last_status_update = None self._last_status_update = None
self._status = None self._status = None
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
_LOGGER.info("Client connection error, marking %s as offline", _LOGGER.info(
self._name) "Client connection error, marking %s as offline", self._name)
raise raise
async def async_trigger_sync_on_all(self): async def async_trigger_sync_on_all(self):
@ -416,8 +426,8 @@ class BluesoundPlayer(MediaPlayerDevice):
await player.force_update_sync_status() await player.force_update_sync_status()
@Throttle(SYNC_STATUS_INTERVAL) @Throttle(SYNC_STATUS_INTERVAL)
async def async_update_sync_status(self, on_updated_cb=None, async def async_update_sync_status(
raise_timeout=False): self, on_updated_cb=None, raise_timeout=False):
"""Update sync status.""" """Update sync status."""
await self.force_update_sync_status( await self.force_update_sync_status(
on_updated_cb, raise_timeout=False) on_updated_cb, raise_timeout=False)
@ -465,7 +475,7 @@ class BluesoundPlayer(MediaPlayerDevice):
'image': item.get('@image', ''), 'image': item.get('@image', ''),
'is_raw_url': True, 'is_raw_url': True,
'url2': item.get('@url', ''), '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']: if 'presets' in resp and 'preset' in resp['presets']:
@ -503,11 +513,6 @@ class BluesoundPlayer(MediaPlayerDevice):
return self._services_items return self._services_items
@property
def should_poll(self):
"""No need to poll information."""
return True
@property @property
def media_content_type(self): def media_content_type(self):
"""Content type of current playing media.""" """Content type of current playing media."""
@ -803,22 +808,22 @@ class BluesoundPlayer(MediaPlayerDevice):
async def async_add_slave(self, slave_device): async def async_add_slave(self, slave_device):
"""Add slave to master.""" """Add slave to master."""
return self.send_bluesound_command('/AddSlave?slave={}&port={}' return await self.send_bluesound_command(
.format(slave_device.host, '/AddSlave?slave={}&port={}'.format(
slave_device.port)) slave_device.host, slave_device.port))
async def async_remove_slave(self, slave_device): async def async_remove_slave(self, slave_device):
"""Remove slave to master.""" """Remove slave to master."""
return self.send_bluesound_command('/RemoveSlave?slave={}&port={}' return await self.send_bluesound_command(
.format(slave_device.host, '/RemoveSlave?slave={}&port={}'.format(
slave_device.port)) slave_device.host, slave_device.port))
async def async_increase_timer(self): async def async_increase_timer(self):
"""Increase sleep time on player.""" """Increase sleep time on player."""
sleep_time = await self.send_bluesound_command('/Sleep') sleep_time = await self.send_bluesound_command('/Sleep')
if sleep_time is None: if sleep_time is None:
_LOGGER.error('Error while increasing sleep time on player: %s', _LOGGER.error(
self.host) "Error while increasing sleep time on player: %s", self.host)
return 0 return 0
return int(sleep_time.get('sleep', '0')) return int(sleep_time.get('sleep', '0'))
@ -831,8 +836,9 @@ class BluesoundPlayer(MediaPlayerDevice):
async def async_set_shuffle(self, shuffle): async def async_set_shuffle(self, shuffle):
"""Enable or disable shuffle mode.""" """Enable or disable shuffle mode."""
return self.send_bluesound_command('/Shuffle?state={}' value = '1' if shuffle else '0'
.format('1' if shuffle else '0')) return await self.send_bluesound_command(
'/Shuffle?state={}'.format(value))
async def async_select_source(self, source): async def async_select_source(self, source):
"""Select input source.""" """Select input source."""
@ -856,14 +862,14 @@ class BluesoundPlayer(MediaPlayerDevice):
if 'is_raw_url' in selected_source and selected_source['is_raw_url']: if 'is_raw_url' in selected_source and selected_source['is_raw_url']:
url = selected_source['url'] url = selected_source['url']
return self.send_bluesound_command(url) return await self.send_bluesound_command(url)
async def async_clear_playlist(self): async def async_clear_playlist(self):
"""Clear players playlist.""" """Clear players playlist."""
if self.is_grouped and not self.is_master: if self.is_grouped and not self.is_master:
return return
return self.send_bluesound_command('Clear') return await self.send_bluesound_command('Clear')
async def async_media_next_track(self): async def async_media_next_track(self):
"""Send media_next command to media player.""" """Send media_next command to media player."""
@ -877,7 +883,7 @@ class BluesoundPlayer(MediaPlayerDevice):
action['@name'] == 'skip'): action['@name'] == 'skip'):
cmd = action['@url'] cmd = action['@url']
return self.send_bluesound_command(cmd) return await self.send_bluesound_command(cmd)
async def async_media_previous_track(self): async def async_media_previous_track(self):
"""Send media_previous command to media player.""" """Send media_previous command to media player."""
@ -891,35 +897,36 @@ class BluesoundPlayer(MediaPlayerDevice):
action['@name'] == 'back'): action['@name'] == 'back'):
cmd = action['@url'] cmd = action['@url']
return self.send_bluesound_command(cmd) return await self.send_bluesound_command(cmd)
async def async_media_play(self): async def async_media_play(self):
"""Send media_play command to media player.""" """Send media_play command to media player."""
if self.is_grouped and not self.is_master: if self.is_grouped and not self.is_master:
return return
return self.send_bluesound_command('Play') return await self.send_bluesound_command('Play')
async def async_media_pause(self): async def async_media_pause(self):
"""Send media_pause command to media player.""" """Send media_pause command to media player."""
if self.is_grouped and not self.is_master: if self.is_grouped and not self.is_master:
return return
return self.send_bluesound_command('Pause') return await self.send_bluesound_command('Pause')
async def async_media_stop(self): async def async_media_stop(self):
"""Send stop command.""" """Send stop command."""
if self.is_grouped and not self.is_master: if self.is_grouped and not self.is_master:
return return
return self.send_bluesound_command('Pause') return await self.send_bluesound_command('Pause')
async def async_media_seek(self, position): async def async_media_seek(self, position):
"""Send media_seek command to media player.""" """Send media_seek command to media player."""
if self.is_grouped and not self.is_master: if self.is_grouped and not self.is_master:
return 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): async def async_play_media(self, media_type, media_id, **kwargs):
""" """
@ -933,9 +940,9 @@ class BluesoundPlayer(MediaPlayerDevice):
url = 'Play?url={}'.format(media_id) url = 'Play?url={}'.format(media_id)
if kwargs.get(ATTR_MEDIA_ENQUEUE): 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): async def async_volume_up(self):
"""Volume up the media player.""" """Volume up the media player."""
@ -957,7 +964,7 @@ class BluesoundPlayer(MediaPlayerDevice):
volume = 0 volume = 0
elif volume > 1: elif volume > 1:
volume = 1 volume = 1
return self.send_bluesound_command( return await self.send_bluesound_command(
'Volume?level=' + str(float(volume) * 100)) 'Volume?level=' + str(float(volume) * 100))
async def async_mute_volume(self, mute): async def async_mute_volume(self, mute):
@ -966,7 +973,7 @@ class BluesoundPlayer(MediaPlayerDevice):
volume = self.volume_level volume = self.volume_level
if volume > 0: if volume > 0:
self._lastvol = volume self._lastvol = volume
return self.send_bluesound_command('Volume?level=0') return await self.send_bluesound_command('Volume?level=0')
else: else:
return self.send_bluesound_command( return await self.send_bluesound_command(
'Volume?level=' + str(float(self._lastvol) * 100)) 'Volume?level=' + str(float(self._lastvol) * 100))

View file

@ -288,7 +288,8 @@ class CastDevice(MediaPlayerDevice):
self._chromecast = None # type: Optional[pychromecast.Chromecast] self._chromecast = None # type: Optional[pychromecast.Chromecast]
self.cast_status = None self.cast_status = None
self.media_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._available = False # type: bool
self._status_listener = None # type: Optional[CastStatusListener] self._status_listener = None # type: Optional[CastStatusListener]
@ -361,7 +362,8 @@ class CastDevice(MediaPlayerDevice):
self._chromecast = None self._chromecast = None
self.cast_status = None self.cast_status = None
self.media_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.invalidate()
self._status_listener = None self._status_listener = None
@ -388,8 +390,36 @@ class CastDevice(MediaPlayerDevice):
def new_media_status(self, media_status): def new_media_status(self, media_status):
"""Handle updates of the 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 = media_status
self.media_status_received = dt_util.utcnow()
self.schedule_update_ha_state() self.schedule_update_ha_state()
def new_connection_status(self, connection_status): def new_connection_status(self, connection_status):
@ -595,13 +625,7 @@ class CastDevice(MediaPlayerDevice):
@property @property
def media_position(self): def media_position(self):
"""Position of current playing media in seconds.""" """Position of current playing media in seconds."""
if self.media_status is None or \ return self.media_status_position
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
@property @property
def media_position_updated_at(self): def media_position_updated_at(self):
@ -609,7 +633,7 @@ class CastDevice(MediaPlayerDevice):
Returns value from homeassistant.util.dt.utcnow(). Returns value from homeassistant.util.dt.utcnow().
""" """
return self.media_status_received return self.media_status_position_received
@property @property
def unique_id(self) -> Optional[str]: def unique_id(self) -> Optional[str]:

View file

@ -8,6 +8,7 @@ import asyncio
from collections import OrderedDict from collections import OrderedDict
from functools import wraps from functools import wraps
import logging import logging
import socket
import urllib import urllib
import re 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): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Kodi platform.""" """Set up the Kodi platform."""
if DATA_KODI not in hass.data: if DATA_KODI not in hass.data:
hass.data[DATA_KODI] = [] hass.data[DATA_KODI] = dict()
# Is this a manual configuration?
if discovery_info is None:
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
port = config.get(CONF_PORT) port = config.get(CONF_PORT)
tcp_port = config.get(CONF_TCP_PORT) tcp_port = config.get(CONF_TCP_PORT)
encryption = config.get(CONF_PROXY_SSL) encryption = config.get(CONF_PROXY_SSL)
websocket = config.get(CONF_ENABLE_WEBSOCKET) 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( entity = KodiDevice(
hass, 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), turn_off_action=config.get(CONF_TURN_OFF_ACTION),
timeout=config.get(CONF_TIMEOUT), websocket=websocket) 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) async_add_devices([entity], update_before_add=True)
@asyncio.coroutine @asyncio.coroutine
@ -189,10 +206,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if key != 'entity_id'} if key != 'entity_id'}
entity_ids = service.data.get('entity_id') entity_ids = service.data.get('entity_id')
if entity_ids: 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] if player.entity_id in entity_ids]
else: else:
target_players = hass.data[DATA_KODI] target_players = hass.data[DATA_KODI].values()
update_tasks = [] update_tasks = []
for player in target_players: for player in target_players:

View file

@ -8,6 +8,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_OFF, MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK,
@ -20,11 +21,11 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, STATE_OFF, CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, STATE_OFF,
CONF_TIMEOUT, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, CONF_TIMEOUT, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY,
STATE_UNAVAILABLE STATE_UNAVAILABLE, EVENT_HOMEASSISTANT_STOP
) )
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pymediaroom==0.6'] REQUIREMENTS = ['pymediaroom==0.6.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -81,12 +82,21 @@ async def async_setup_platform(hass, config, async_add_devices,
if not config[CONF_OPTIMISTIC]: if not config[CONF_OPTIMISTIC]:
from pymediaroom import install_mediaroom_protocol 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: if not already_installed:
await install_mediaroom_protocol( hass.data[DISCOVERY_MEDIAROOM] = await install_mediaroom_protocol(
responses_callback=callback_notify) 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") _LOGGER.debug("Auto discovery installed")
hass.data[DISCOVERY_MEDIAROOM] = True
class MediaroomDevice(MediaPlayerDevice): class MediaroomDevice(MediaPlayerDevice):
@ -120,7 +130,7 @@ class MediaroomDevice(MediaPlayerDevice):
self._channel = None self._channel = None
self._optimistic = optimistic self._optimistic = optimistic
self._state = STATE_PLAYING if optimistic else STATE_STANDBY 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 self._available = True
if device_id: if device_id:
self._unique_id = device_id self._unique_id = device_id

View file

@ -23,7 +23,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ['python-mpd2==0.5.5'] REQUIREMENTS = ['python-mpd2==1.0.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -22,6 +22,7 @@ REQUIREMENTS = ['onkyo-eiscp==1.2.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_SOURCES = 'sources' CONF_SOURCES = 'sources'
CONF_ZONE2 = 'zone2'
DEFAULT_NAME = 'Onkyo Receiver' 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_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES):
{cv.string: cv.string}, {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), eiscp.eISCP(host), config.get(CONF_SOURCES),
name=config.get(CONF_NAME))) name=config.get(CONF_NAME)))
KNOWN_HOSTS.append(host) 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: except OSError:
_LOGGER.error("Unable to connect to receiver at %s", host) _LOGGER.error("Unable to connect to receiver at %s", host)
else: else:
@ -98,8 +108,9 @@ class OnkyoDevice(MediaPlayerDevice):
return result return result
def update(self): def update(self):
"""Get the latest details from the device.""" """Get the latest state from the device."""
status = self.command('system-power query') status = self.command('system-power query')
if not status: if not status:
return return
if status[1] == 'on': if status[1] == 'on':
@ -107,9 +118,11 @@ class OnkyoDevice(MediaPlayerDevice):
else: else:
self._pwstate = STATE_OFF self._pwstate = STATE_OFF
return return
volume_raw = self.command('volume query') volume_raw = self.command('volume query')
mute_raw = self.command('audio-muting query') mute_raw = self.command('audio-muting query')
current_source_raw = self.command('input-selector query') current_source_raw = self.command('input-selector query')
if not (volume_raw and mute_raw and current_source_raw): if not (volume_raw and mute_raw and current_source_raw):
return return
@ -147,12 +160,12 @@ class OnkyoDevice(MediaPlayerDevice):
@property @property
def is_volume_muted(self): def is_volume_muted(self):
"""Boolean if volume is currently muted.""" """Return boolean indicating mute status."""
return self._muted return self._muted
@property @property
def supported_features(self): def supported_features(self):
"""Flag media player features that are supported.""" """Return media player features that are supported."""
return SUPPORT_ONKYO return SUPPORT_ONKYO
@property @property
@ -166,7 +179,7 @@ class OnkyoDevice(MediaPlayerDevice):
return self._source_list return self._source_list
def turn_off(self): def turn_off(self):
"""Turn off media player.""" """Turn the media player off."""
self.command('system-power standby') self.command('system-power standby')
def set_volume_level(self, volume): def set_volume_level(self, volume):
@ -189,3 +202,68 @@ class OnkyoDevice(MediaPlayerDevice):
if source in self._source_list: if source in self._source_list:
source = self._reverse_mapping[source] source = self._reverse_mapping[source]
self.command('input-selector {}'.format(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))

View file

@ -402,3 +402,13 @@ songpal_set_sound_setting:
value: value:
description: Value to set. description: Value to set.
example: 'on' 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'

View file

@ -266,6 +266,8 @@ class SqueezeBoxDevice(MediaPlayerDevice):
if response is False: if response is False:
return return
last_media_position = self.media_position
self._status = {} self._status = {}
try: try:
@ -278,6 +280,10 @@ class SqueezeBoxDevice(MediaPlayerDevice):
pass pass
self._status.update(response) self._status.update(response)
if self.media_position != last_media_position:
_LOGGER.debug('Media position updated for %s: %s',
self, self.media_position)
self._last_update = utcnow() self._last_update = utcnow()
@property @property

View file

@ -344,6 +344,42 @@ class LgWebOSDevice(MediaPlayerDevice):
self._current_source = source_dict['label'] self._current_source = source_dict['label']
self._client.set_input(source_dict['id']) 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): def media_play(self):
"""Send play command.""" """Send play command."""
self._playing = True self._playing = True

View file

@ -37,7 +37,8 @@ PLATFORM_SCHEMA = vol.Schema(
vol.All(PLATFORM_SCHEMA.extend({ vol.All(PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_API_KEY): 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, vol.Optional(CONF_SENDER): cv.string,
}), validate_sender)) }), validate_sender))
@ -59,20 +60,18 @@ class ClicksendNotificationService(BaseNotificationService):
"""Initialize the service.""" """Initialize the service."""
self.username = config.get(CONF_USERNAME) self.username = config.get(CONF_USERNAME)
self.api_key = config.get(CONF_API_KEY) 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) self.sender = config.get(CONF_SENDER, CONF_RECIPIENT)
def send_message(self, message="", **kwargs): def send_message(self, message="", **kwargs):
"""Send a message to a user.""" """Send a message to a user."""
data = ({ data = {"messages": []}
'messages': [ for recipient in self.recipients:
{ data["messages"].append({
'source': 'hass.notify', 'source': 'hass.notify',
'from': self.sender, 'from': self.sender,
'to': self.recipient, 'to': recipient,
'body': message, 'body': message,
}
]
}) })
api_url = "{}/sms/send".format(BASE_API_URL) api_url = "{}/sms/send".format(BASE_API_URL)

View file

@ -4,6 +4,7 @@ Facebook platform for notify component.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.facebook/ https://home-assistant.io/components/notify.facebook/
""" """
import json
import logging import logging
from aiohttp.hdrs import CONTENT_TYPE from aiohttp.hdrs import CONTENT_TYPE
@ -19,6 +20,8 @@ _LOGGER = logging.getLogger(__name__)
CONF_PAGE_ACCESS_TOKEN = 'page_access_token' CONF_PAGE_ACCESS_TOKEN = 'page_access_token'
BASE_URL = 'https://graph.facebook.com/v2.6/me/messages' 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({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PAGE_ACCESS_TOKEN): cv.string, vol.Required(CONF_PAGE_ACCESS_TOKEN): cv.string,
@ -55,8 +58,36 @@ class FacebookNotificationService(BaseNotificationService):
_LOGGER.error("At least 1 target is required") _LOGGER.error("At least 1 target is required")
return return
# broadcast message
if targets[0].lower() == 'broadcast':
broadcast_create_body = {"messages": [body_message]}
_LOGGER.debug("Broadcast body %s : ", broadcast_create_body)
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",
}
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:
log_error(resp)
# non-broadcast message
else:
for target in targets: for target in targets:
# If the target starts with a "+", we suppose it's a phone number, # If the target starts with a "+", it's a phone number,
# otherwise it's a user id. # otherwise it's a user id.
if target.startswith('+'): if target.startswith('+'):
recipient = {"phone_number": target} recipient = {"phone_number": target}
@ -67,15 +98,20 @@ class FacebookNotificationService(BaseNotificationService):
"recipient": recipient, "recipient": recipient,
"message": body_message "message": body_message
} }
import json
resp = requests.post(BASE_URL, data=json.dumps(body), resp = requests.post(BASE_URL, data=json.dumps(body),
params=payload, params=payload,
headers={CONTENT_TYPE: CONTENT_TYPE_JSON}, headers={CONTENT_TYPE: CONTENT_TYPE_JSON},
timeout=10) timeout=10)
if resp.status_code != 200: if resp.status_code != 200:
obj = resp.json() log_error(resp)
def log_error(response):
"""Log error message."""
obj = response.json()
error_message = obj['error']['message'] error_message = obj['error']['message']
error_code = obj['error']['code'] error_code = obj['error']['code']
_LOGGER.error( _LOGGER.error(
"Error %s : %s (Code %s)", resp.status_code, error_message, "Error %s : %s (Code %s)", response.status_code, error_message,
error_code) error_code)

View file

@ -76,8 +76,6 @@ def send_message(sender, password, recipient, use_tls,
"""Initialize the Jabber Bot.""" """Initialize the Jabber Bot."""
super(SendNotificationBot, self).__init__(sender, password) super(SendNotificationBot, self).__init__(sender, password)
logging.basicConfig(level=logging.ERROR)
self.use_tls = use_tls self.use_tls = use_tls
self.use_ipv6 = False self.use_ipv6 = False
self.add_event_handler('failed_auth', self.check_credentials) self.add_event_handler('failed_auth', self.check_credentials)

View file

@ -185,6 +185,9 @@ class Metrics(object):
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
metric = state.entity_id.split(".")[1] metric = state.entity_id.split(".")[1]
if '_' not in str(metric):
metric = state.entity_id.replace('.', '_')
try: try:
int(metric.split("_")[-1]) int(metric.split("_")[-1])
metric = "_".join(metric.split("_")[:-1]) metric = "_".join(metric.split("_")[:-1])

View file

@ -18,7 +18,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.light import ATTR_BRIGHTNESS
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyqwikswitch==0.6'] REQUIREMENTS = ['pyqwikswitch==0.71']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -34,17 +34,54 @@ CONFIG_SCHEMA = vol.Schema({
vol.Coerce(str), vol.Coerce(str),
vol.Optional(CONF_DIMMER_ADJUST, default=1): CV_DIM_VALUE, vol.Optional(CONF_DIMMER_ADJUST, default=1): CV_DIM_VALUE,
vol.Optional(CONF_BUTTON_EVENTS, default=[]): cv.ensure_list_csv, 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( vol.Optional(CONF_SWITCHES, default=[]): vol.All(
cv.ensure_list, [str]) cv.ensure_list, [str])
})}, extra=vol.ALLOW_EXTRA) })}, extra=vol.ALLOW_EXTRA)
class QSToggleEntity(Entity): class QSEntity(Entity):
"""Representation of a Qwikswitch Entity. """Qwikswitch Entity base."""
Implement base QS methods. Modeled around HA ToggleEntity[1] & should only def __init__(self, qsid, name):
be used in a class that extends both QSToggleEntity *and* ToggleEntity. """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: Implemented:
- QSLight extends QSToggleEntity and Light[2] (ToggleEntity[1]) - QSLight extends QSToggleEntity and Light[2] (ToggleEntity[1])
@ -57,52 +94,28 @@ class QSToggleEntity(Entity):
def __init__(self, qsid, qsusb): def __init__(self, qsid, qsusb):
"""Initialize the ToggleEntity.""" """Initialize the ToggleEntity."""
from pyqwikswitch import (QS_NAME, QSDATA, QS_TYPE, QSType) self.device = qsusb.devices[qsid]
self.qsid = qsid super().__init__(qsid, self.device.name)
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
@property @property
def is_on(self): def is_on(self):
"""Check if device is on (non-zero).""" """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): async def async_turn_on(self, **kwargs):
"""Turn the device on.""" """Turn the device on."""
new = kwargs.get(ATTR_BRIGHTNESS, 255) 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, **_): async def async_turn_off(self, **_):
"""Turn the device off.""" """Turn the device off."""
self._qsusb.set_value(self.qsid, 0) self.hass.data[DOMAIN].devices.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)
async def async_setup(hass, config): async def async_setup(hass, config):
"""Qwiskswitch component setup.""" """Qwiskswitch component setup."""
from pyqwikswitch.async_ import QSUsb from pyqwikswitch.async_ import QSUsb
from pyqwikswitch import ( from pyqwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType
CMD_BUTTONS, QS_CMD, QS_ID, QS_TYPE, QSType)
# Add cmd's to in /&listen packets will fire events # Add cmd's to in /&listen packets will fire events
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] # 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] url = config[DOMAIN][CONF_URL]
dimmer_adjust = config[DOMAIN][CONF_DIMMER_ADJUST] dimmer_adjust = config[DOMAIN][CONF_DIMMER_ADJUST]
sensors = config[DOMAIN]['sensors'] sensors = config[DOMAIN][CONF_SENSORS]
switches = config[DOMAIN]['switches'] switches = config[DOMAIN][CONF_SWITCHES]
def callback_value_changed(_qsd, qsid, _val): def callback_value_changed(_qsd, qsid, _val):
"""Update entity values based on device change.""" """Update entity values based on device change."""
@ -131,17 +144,17 @@ async def async_setup(hass, config):
hass.data[DOMAIN] = qsusb hass.data[DOMAIN] = qsusb
_new = {'switch': [], 'light': [], 'sensor': sensors} _new = {'switch': [], 'light': [], 'sensor': sensors}
for _id, item in qsusb.devices: for qsid, dev in qsusb.devices.items():
if _id in switches: if qsid in switches:
if item[QS_TYPE] != QSType.relay: if dev.qstype != QSType.relay:
_LOGGER.warning( _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 continue
_new['switch'].append(_id) _new['switch'].append(qsid)
elif item[QS_TYPE] in [QSType.relay, QSType.dimmer]: elif dev.qstype in (QSType.relay, QSType.dimmer):
_new['light'].append(_id) _new['light'].append(qsid)
else: else:
_LOGGER.warning("Ignored unknown QSUSB device: %s", item) _LOGGER.warning("Ignored unknown QSUSB device: %s", dev)
continue continue
# Load platforms # Load platforms
@ -149,24 +162,21 @@ async def async_setup(hass, config):
if comp_conf: if comp_conf:
load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config) 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.""" """Typically a button press or update signal."""
# If button pressed, fire a hass event # If button pressed, fire a hass event
if QS_ID in item: if QS_ID in qspacket:
if item.get(QS_CMD, '') in cmd_buttons: if qspacket.get(QS_CMD, '') in cmd_buttons:
hass.bus.async_fire( hass.bus.async_fire(
'qwikswitch.button.{}'.format(item[QS_ID]), item) 'qwikswitch.button.{}'.format(qspacket[QS_ID]), qspacket)
return return
# Private method due to bad __iter__ design in qsusb if qspacket[QS_ID] not in qsusb.devices:
# qsusb.devices returns a list of tuples
if item[QS_ID] not in \
qsusb.devices._data: # pylint: disable=protected-access
# Not a standard device in, component can handle packet # Not a standard device in, component can handle packet
# i.e. sensors # 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( hass.helpers.dispatcher.async_dispatcher_send(
item[QS_ID], item) qspacket[QS_ID], qspacket)
# Update all ha_objects # Update all ha_objects
hass.async_add_job(qsusb.update_from_devices) hass.async_add_job(qsusb.update_from_devices)

View file

@ -35,7 +35,7 @@ from . import migration, purge
from .const import DATA_INSTANCE from .const import DATA_INSTANCE
from .util import session_scope from .util import session_scope
REQUIREMENTS = ['sqlalchemy==1.2.5'] REQUIREMENTS = ['sqlalchemy==1.2.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -47,9 +47,8 @@ ATTR_KEEP_DAYS = 'keep_days'
ATTR_REPACK = 'repack' ATTR_REPACK = 'repack'
SERVICE_PURGE_SCHEMA = vol.Schema({ SERVICE_PURGE_SCHEMA = vol.Schema({
vol.Optional(ATTR_KEEP_DAYS): vol.Optional(ATTR_KEEP_DAYS): vol.All(vol.Coerce(int), vol.Range(min=0)),
vol.All(vol.Coerce(int), vol.Range(min=0)), vol.Optional(ATTR_REPACK, default=False): cv.boolean,
vol.Optional(ATTR_REPACK, default=False): cv.boolean
}) })
DEFAULT_URL = 'sqlite:///{hass_config_path}' DEFAULT_URL = 'sqlite:///{hass_config_path}'

View file

@ -8,6 +8,8 @@ https://home-assistant.io/components/sensor/
from datetime import timedelta from datetime import timedelta
import logging import logging
import voluptuous as vol
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
@ -18,6 +20,13 @@ DOMAIN = 'sensor'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
SCAN_INTERVAL = timedelta(seconds=30) 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): async def async_setup(hass, config):

View file

@ -15,7 +15,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['alpha_vantage==1.9.0'] REQUIREMENTS = ['alpha_vantage==2.0.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -52,6 +52,7 @@ class BMWConnectedDriveSensor(Entity):
self._state = None self._state = None
self._unit_of_measurement = None self._unit_of_measurement = None
self._name = '{} {}'.format(self._vehicle.name, self._attribute) self._name = '{} {}'.format(self._vehicle.name, self._attribute)
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
self._sensor_name = sensor_name self._sensor_name = sensor_name
self._icon = icon self._icon = icon
@ -60,6 +61,11 @@ class BMWConnectedDriveSensor(Entity):
"""Data update is triggered from BMWConnectedDriveEntity.""" """Data update is triggered from BMWConnectedDriveEntity."""
return False return False
@property
def unique_id(self):
"""Return the unique ID of the sensor."""
return self._unique_id
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of the sensor.""" """Return the name of the sensor."""
@ -86,7 +92,7 @@ class BMWConnectedDriveSensor(Entity):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the binary sensor.""" """Return the state attributes of the sensor."""
return { return {
'car': self._vehicle.name 'car': self._vehicle.name
} }

View file

@ -56,9 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
timeout = config.get(CONF_TIMEOUT) timeout = config.get(CONF_TIMEOUT)
update_interval = config.get(CONF_UPDATE_INTERVAL) update_interval = config.get(CONF_UPDATE_INTERVAL)
broadlink_data = BroadlinkData(update_interval, host, mac_addr, timeout) broadlink_data = BroadlinkData(update_interval, host, mac_addr, timeout)
dev = [] dev = []
for variable in config[CONF_MONITORED_CONDITIONS]: for variable in config[CONF_MONITORED_CONDITIONS]:
dev.append(BroadlinkSensor(name, broadlink_data, variable)) dev.append(BroadlinkSensor(name, broadlink_data, variable))
@ -104,10 +102,11 @@ class BroadlinkData(object):
def __init__(self, interval, ip_addr, mac_addr, timeout): def __init__(self, interval, ip_addr, mac_addr, timeout):
"""Initialize the data object.""" """Initialize the data object."""
import broadlink
self.data = None self.data = None
self._device = broadlink.a1((ip_addr, 80), mac_addr, None) self.ip_addr = ip_addr
self._device.timeout = timeout self.mac_addr = mac_addr
self.timeout = timeout
self._connect()
self._schema = vol.Schema({ self._schema = vol.Schema({
vol.Optional('temperature'): vol.Range(min=-50, max=150), vol.Optional('temperature'): vol.Range(min=-50, max=150),
vol.Optional('humidity'): vol.Range(min=0, max=100), vol.Optional('humidity'): vol.Range(min=0, max=100),
@ -119,6 +118,11 @@ class BroadlinkData(object):
if not self._auth(): if not self._auth():
_LOGGER.warning("Failed to connect to device") _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): def _update(self, retry=3):
try: try:
data = self._device.check_sensors_raw() data = self._device.check_sensors_raw()
@ -140,5 +144,6 @@ class BroadlinkData(object):
except socket.timeout: except socket.timeout:
auth = False auth = False
if not auth and retry > 0: if not auth and retry > 0:
self._connect()
return self._auth(retry-1) return self._auth(retry-1)
return auth return auth

View file

@ -16,6 +16,7 @@ from homeassistant.util import slugify
DEPENDENCIES = ['deconz'] DEPENDENCIES = ['deconz']
ATTR_CURRENT = 'current' ATTR_CURRENT = 'current'
ATTR_DAYLIGHT = 'daylight'
ATTR_EVENT_ID = 'event_id' ATTR_EVENT_ID = 'event_id'
@ -113,6 +114,8 @@ class DeconzSensor(Entity):
if self.unit_of_measurement == 'Watts': if self.unit_of_measurement == 'Watts':
attr[ATTR_CURRENT] = self._sensor.current attr[ATTR_CURRENT] = self._sensor.current
attr[ATTR_VOLTAGE] = self._sensor.voltage attr[ATTR_VOLTAGE] = self._sensor.voltage
if self._sensor.sensor_class == 'daylight':
attr[ATTR_DAYLIGHT] = self._sensor.daylight
return attr return attr

View file

@ -19,7 +19,8 @@ from homeassistant.const import (
CONF_NAME, CONF_MONITORED_VARIABLES) CONF_NAME, CONF_MONITORED_VARIABLES)
from homeassistant.helpers.entity import Entity 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__) _LOGGER = logging.getLogger(__name__)

View file

@ -52,6 +52,13 @@ class EcobeeSensor(Entity):
"""Return the name of the Ecobee sensor.""" """Return the name of the Ecobee sensor."""
return self._name 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 @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""

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