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

View file

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

View file

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

View file

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

View file

@ -42,7 +42,8 @@ def from_config_dict(config: Dict[str, Any],
verbose: bool = False,
skip_pip: bool = False,
log_rotate_days: Any = None,
log_file: Any = None) \
log_file: Any = None,
log_no_color: bool = False) \
-> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary.
@ -60,7 +61,7 @@ def from_config_dict(config: Dict[str, Any],
hass = hass.loop.run_until_complete(
async_from_config_dict(
config, hass, config_dir, enable_log, verbose, skip_pip,
log_rotate_days, log_file)
log_rotate_days, log_file, log_no_color)
)
return hass
@ -74,7 +75,8 @@ def async_from_config_dict(config: Dict[str, Any],
verbose: bool = False,
skip_pip: bool = False,
log_rotate_days: Any = None,
log_file: Any = None) \
log_file: Any = None,
log_no_color: bool = False) \
-> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary.
@ -84,7 +86,8 @@ def async_from_config_dict(config: Dict[str, Any],
start = time()
if enable_log:
async_enable_logging(hass, verbose, log_rotate_days, log_file)
async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color)
core_config = config.get(core.DOMAIN, {})
@ -164,7 +167,8 @@ def from_config_file(config_path: str,
verbose: bool = False,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None):
log_file: Any = None,
log_no_color: bool = False):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given,
@ -176,7 +180,8 @@ def from_config_file(config_path: str,
# run task
hass = hass.loop.run_until_complete(
async_from_config_file(
config_path, hass, verbose, skip_pip, log_rotate_days, log_file)
config_path, hass, verbose, skip_pip,
log_rotate_days, log_file, log_no_color)
)
return hass
@ -188,7 +193,8 @@ def async_from_config_file(config_path: str,
verbose: bool = False,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None):
log_file: Any = None,
log_no_color: bool = False):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter.
@ -199,7 +205,8 @@ def async_from_config_file(config_path: str,
hass.config.config_dir = config_dir
yield from async_mount_local_lib_path(config_dir, hass.loop)
async_enable_logging(hass, verbose, log_rotate_days, log_file)
async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color)
try:
config_dict = yield from hass.async_add_job(
@ -216,40 +223,51 @@ def async_from_config_file(config_path: str,
@core.callback
def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False,
log_rotate_days=None, log_file=None) -> None:
def async_enable_logging(hass: core.HomeAssistant,
verbose: bool = False,
log_rotate_days=None,
log_file=None,
log_no_color: bool = False) -> None:
"""Set up the logging.
This method must be run in the event loop.
"""
logging.basicConfig(level=logging.INFO)
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s")
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
datefmt = '%Y-%m-%d %H:%M:%S'
if not log_no_color:
try:
from colorlog import ColoredFormatter
# basicConfig must be called after importing colorlog in order to
# ensure that the handlers it sets up wraps the correct streams.
logging.basicConfig(level=logging.INFO)
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
colorfmt,
datefmt=datefmt,
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
except ImportError:
pass
# If the above initialization failed for any reason, setup the default
# formatting. If the above succeeds, this wil result in a no-op.
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
# Suppress overly verbose logs from libraries that aren't helpful
logging.getLogger('requests').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('aiohttp.access').setLevel(logging.WARNING)
try:
from colorlog import ColoredFormatter
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
colorfmt,
datefmt=datefmt,
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
except ImportError:
pass
# Log errors to a file if we have write access to file or config dir
if log_file is None:
err_log_path = hass.config.path(ERROR_LOG_FILENAME)

View file

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

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

View file

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

View file

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

View file

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

View file

@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'bmw_connected_drive'
CONF_REGION = 'region'
ATTR_VIN = 'vin'
ACCOUNT_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
@ -35,35 +35,40 @@ CONFIG_SCHEMA = vol.Schema({
},
}, extra=vol.ALLOW_EXTRA)
SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_VIN): cv.string,
})
BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor']
UPDATE_INTERVAL = 5 # in minutes
SERVICE_UPDATE_STATE = 'update_state'
def setup(hass, config):
_SERVICE_MAP = {
'light_flash': 'trigger_remote_light_flash',
'sound_horn': 'trigger_remote_horn',
'activate_air_conditioning': 'trigger_remote_air_conditioning',
}
def setup(hass, config: dict):
"""Set up the BMW connected drive components."""
accounts = []
for name, account_config in config[DOMAIN].items():
username = account_config[CONF_USERNAME]
password = account_config[CONF_PASSWORD]
region = account_config[CONF_REGION]
_LOGGER.debug('Adding new account %s', name)
bimmer = BMWConnectedDriveAccount(username, password, region, name)
accounts.append(bimmer)
# update every UPDATE_INTERVAL minutes, starting now
# this should even out the load on the servers
now = datetime.datetime.now()
track_utc_time_change(
hass, bimmer.update,
minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
second=now.second)
accounts.append(setup_account(account_config, hass, name))
hass.data[DOMAIN] = accounts
for account in accounts:
account.update()
def _update_all(call) -> None:
"""Update all BMW accounts."""
for cd_account in hass.data[DOMAIN]:
cd_account.update()
# Service to manually trigger updates for all accounts.
hass.services.register(DOMAIN, SERVICE_UPDATE_STATE, _update_all)
_update_all(None)
for component in BMW_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
@ -71,6 +76,48 @@ def setup(hass, config):
return True
def setup_account(account_config: dict, hass, name: str) \
-> 'BMWConnectedDriveAccount':
"""Set up a new BMWConnectedDriveAccount based on the config."""
username = account_config[CONF_USERNAME]
password = account_config[CONF_PASSWORD]
region = account_config[CONF_REGION]
_LOGGER.debug('Adding new account %s', name)
cd_account = BMWConnectedDriveAccount(username, password, region, name)
def execute_service(call):
"""Execute a service for a vehicle.
This must be a member function as we need access to the cd_account
object here.
"""
vin = call.data[ATTR_VIN]
vehicle = cd_account.account.get_vehicle(vin)
if not vehicle:
_LOGGER.error('Could not find a vehicle for VIN "%s"!', vin)
return
function_name = _SERVICE_MAP[call.service]
function_call = getattr(vehicle.remote_services, function_name)
function_call()
# register the remote services
for service in _SERVICE_MAP:
hass.services.register(
DOMAIN, service,
execute_service,
schema=SERVICE_SCHEMA)
# update every UPDATE_INTERVAL minutes, starting now
# this should even out the load on the servers
now = datetime.datetime.now()
track_utc_time_change(
hass, cd_account.update,
minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
second=now.second)
return cd_account
class BMWConnectedDriveAccount(object):
"""Representation of a BMW vehicle."""

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

View file

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

View file

@ -41,6 +41,14 @@ CONTENT = 'content'
DESCRIPTION = 'description'
# Calendar Platform: Used in the '_get_date()' method
DATETIME = 'dateTime'
# Service Call: When is this task due (in natural language)?
DUE_DATE_STRING = 'due_date_string'
# Service Call: The language of DUE_DATE_STRING
DUE_DATE_LANG = 'due_date_lang'
# Service Call: The available options of DUE_DATE_LANG
DUE_DATE_VALID_LANGS = ['en', 'da', 'pl', 'zh', 'ko', 'de',
'pt', 'ja', 'it', 'fr', 'sv', 'ru',
'es', 'nl']
# Attribute: When is this task due?
# Service Call: When is this task due?
DUE_DATE = 'due_date'
@ -83,7 +91,11 @@ NEW_TASK_SERVICE_SCHEMA = vol.Schema({
vol.Optional(PROJECT_NAME, default='inbox'): vol.All(cv.string, vol.Lower),
vol.Optional(LABELS): cv.ensure_list_csv,
vol.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)),
vol.Optional(DUE_DATE): cv.string,
vol.Exclusive(DUE_DATE_STRING, 'due_date'): cv.string,
vol.Optional(DUE_DATE_LANG):
vol.All(cv.string, vol.In(DUE_DATE_VALID_LANGS)),
vol.Exclusive(DUE_DATE, 'due_date'): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -186,6 +198,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if PRIORITY in call.data:
item.update(priority=call.data[PRIORITY])
if DUE_DATE_STRING in call.data:
item.update(date_string=call.data[DUE_DATE_STRING])
if DUE_DATE_LANG in call.data:
item.update(date_lang=call.data[DUE_DATE_LANG])
if DUE_DATE in call.data:
due_date = dt.parse_datetime(call.data[DUE_DATE])
if due_date is None:

View file

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

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

View file

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

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
elif operation_mode == STATE_AUTO:
device_mode = NEST_MODE_HEAT_COOL
else:
device_mode = STATE_OFF
_LOGGER.error(
"An error occurred while setting device mode. "
"Invalid operation mode: %s", operation_mode)
self.device.mode = device_mode
@property

View file

@ -1,11 +1,10 @@
"""Http views to control the config manager."""
import asyncio
import voluptuous as vol
from homeassistant import config_entries
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView)
REQUIREMENTS = ['voluptuous-serialize==1']
@ -16,15 +15,17 @@ def async_setup(hass):
"""Enable the Home Assistant views."""
hass.http.register_view(ConfigManagerEntryIndexView)
hass.http.register_view(ConfigManagerEntryResourceView)
hass.http.register_view(ConfigManagerFlowIndexView)
hass.http.register_view(ConfigManagerFlowResourceView)
hass.http.register_view(
ConfigManagerFlowIndexView(hass.config_entries.flow))
hass.http.register_view(
ConfigManagerFlowResourceView(hass.config_entries.flow))
hass.http.register_view(ConfigManagerAvailableFlowView)
return True
def _prepare_json(result):
"""Convert result for JSON."""
if result['type'] != config_entries.RESULT_TYPE_FORM:
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
return result
import voluptuous_serialize
@ -78,7 +79,7 @@ class ConfigManagerEntryResourceView(HomeAssistantView):
return self.json(result)
class ConfigManagerFlowIndexView(HomeAssistantView):
class ConfigManagerFlowIndexView(FlowManagerIndexView):
"""View to create config flows."""
url = '/api/config/config_entries/flow'
@ -94,81 +95,16 @@ class ConfigManagerFlowIndexView(HomeAssistantView):
hass = request.app['hass']
return self.json([
flow for flow in hass.config_entries.flow.async_progress()
if flow['source'] != config_entries.SOURCE_USER])
@RequestDataValidator(vol.Schema({
vol.Required('domain'): str,
}))
@asyncio.coroutine
def post(self, request, data):
"""Handle a POST request."""
hass = request.app['hass']
try:
result = yield from hass.config_entries.flow.async_init(
data['domain'])
except config_entries.UnknownHandler:
return self.json_message('Invalid handler specified', 404)
except config_entries.UnknownStep:
return self.json_message('Handler does not support init', 400)
result = _prepare_json(result)
return self.json(result)
flw for flw in hass.config_entries.flow.async_progress()
if flw['source'] != data_entry_flow.SOURCE_USER])
class ConfigManagerFlowResourceView(HomeAssistantView):
class ConfigManagerFlowResourceView(FlowManagerResourceView):
"""View to interact with the flow manager."""
url = '/api/config/config_entries/flow/{flow_id}'
name = 'api:config:config_entries:flow:resource'
@asyncio.coroutine
def get(self, request, flow_id):
"""Get the current state of a flow."""
hass = request.app['hass']
try:
result = yield from hass.config_entries.flow.async_configure(
flow_id)
except config_entries.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
result = _prepare_json(result)
return self.json(result)
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
@asyncio.coroutine
def post(self, request, flow_id, data):
"""Handle a POST request."""
hass = request.app['hass']
try:
result = yield from hass.config_entries.flow.async_configure(
flow_id, data)
except config_entries.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
except vol.Invalid:
return self.json_message('User input malformed', 400)
result = _prepare_json(result)
return self.json(result)
@asyncio.coroutine
def delete(self, request, flow_id):
"""Cancel a flow in progress."""
hass = request.app['hass']
try:
hass.config_entries.flow.async_abort(flow_id)
except config_entries.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
return self.json_message('Flow aborted')
class ConfigManagerAvailableFlowView(HomeAssistantView):
"""View to query available flows."""

View file

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

View file

@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Tahoma covers."""
"""Set up the Tahoma covers."""
controller = hass.data[TAHOMA_DOMAIN]['controller']
devices = []
for device in hass.data[TAHOMA_DOMAIN]['devices']['cover']:

View file

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

View file

@ -4,28 +4,20 @@ Support for deCONZ devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/deconz/
"""
import logging
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.discovery import SERVICE_DECONZ
from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery, aiohttp_client
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.json import load_json, save_json
from homeassistant.helpers import (
aiohttp_client, discovery, config_validation as cv)
from homeassistant.util.json import load_json
REQUIREMENTS = ['pydeconz==35']
# Loading the config flow file will register the flow
from .config_flow import configured_hosts
from .const import CONFIG_FILE, DATA_DECONZ_ID, DOMAIN, _LOGGER
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'deconz'
DATA_DECONZ_ID = 'deconz_entities'
CONFIG_FILE = 'deconz.conf'
REQUIREMENTS = ['pydeconz==36']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@ -46,46 +38,38 @@ SERVICE_SCHEMA = vol.Schema({
})
CONFIG_INSTRUCTIONS = """
Unlock your deCONZ gateway to register with Home Assistant.
1. [Go to deCONZ system settings](http://{}:{}/edit_system.html)
2. Press "Unlock Gateway" button
[deCONZ platform documentation](https://home-assistant.io/components/deconz/)
"""
async def async_setup(hass, config):
"""Set up services and configuration for deCONZ component."""
result = False
config_file = await hass.async_add_job(
load_json, hass.config.path(CONFIG_FILE))
async def async_deconz_discovered(service, discovery_info):
"""Call when deCONZ gateway has been found."""
deconz_config = {}
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
await async_request_configuration(hass, config, deconz_config)
if config_file:
result = await async_setup_deconz(hass, config, config_file)
if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]:
deconz_config = config[DOMAIN]
if CONF_API_KEY in deconz_config:
result = await async_setup_deconz(hass, config, deconz_config)
else:
await async_request_configuration(hass, config, deconz_config)
return True
if not result:
discovery.async_listen(hass, SERVICE_DECONZ, async_deconz_discovered)
"""Load configuration for deCONZ component.
Discovery has loaded the component if DOMAIN is not present in config.
"""
if DOMAIN in config:
deconz_config = None
config_file = await hass.async_add_job(
load_json, hass.config.path(CONFIG_FILE))
if config_file:
deconz_config = config_file
elif CONF_HOST in config[DOMAIN]:
deconz_config = config[DOMAIN]
if deconz_config and not configured_hosts(hass):
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data=deconz_config
))
return True
async def async_setup_entry(hass, entry):
"""Set up a deCONZ bridge for a config entry."""
if DOMAIN in hass.data:
_LOGGER.error(
"Config entry failed since one deCONZ instance already exists")
return False
result = await async_setup_deconz(hass, None, entry.data)
if result:
return True
return False
async def async_setup_deconz(hass, config, deconz_config):
"""Set up a deCONZ session.
@ -94,8 +78,8 @@ async def async_setup_deconz(hass, config, deconz_config):
"""
_LOGGER.debug("deCONZ config %s", deconz_config)
from pydeconz import DeconzSession
websession = async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, websession, **deconz_config)
session = aiohttp_client.async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, session, **deconz_config)
result = await deconz.async_load_parameters()
if result is False:
_LOGGER.error("Failed to communicate with deCONZ")
@ -152,121 +136,3 @@ async def async_setup_deconz(hass, config, deconz_config):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown)
return True
async def async_request_configuration(hass, config, deconz_config):
"""Request configuration steps from the user."""
configurator = hass.components.configurator
async def async_configuration_callback(data):
"""Set up actions to do when our configuration callback is called."""
from pydeconz.utils import async_get_api_key
websession = async_get_clientsession(hass)
api_key = await async_get_api_key(websession, **deconz_config)
if api_key:
deconz_config[CONF_API_KEY] = api_key
result = await async_setup_deconz(hass, config, deconz_config)
if result:
await hass.async_add_job(
save_json, hass.config.path(CONFIG_FILE), deconz_config)
configurator.async_request_done(request_id)
return
else:
configurator.async_notify_errors(
request_id, "Couldn't load configuration.")
else:
configurator.async_notify_errors(
request_id, "Couldn't get an API key.")
return
instructions = CONFIG_INSTRUCTIONS.format(
deconz_config[CONF_HOST], deconz_config[CONF_PORT])
request_id = configurator.async_request_config(
"deCONZ", async_configuration_callback,
description=instructions,
entity_picture="/static/images/logo_deconz.jpeg",
submit_caption="I have unlocked the gateway",
)
@config_entries.HANDLERS.register(DOMAIN)
class DeconzFlowHandler(config_entries.ConfigFlowHandler):
"""Handle a deCONZ config flow."""
VERSION = 1
def __init__(self):
"""Initialize the deCONZ flow."""
self.bridges = []
self.deconz_config = {}
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
from pydeconz.utils import async_discovery
if DOMAIN in self.hass.data:
return self.async_abort(
reason='one_instance_only'
)
if user_input is not None:
for bridge in self.bridges:
if bridge[CONF_HOST] == user_input[CONF_HOST]:
self.deconz_config = bridge
return await self.async_step_link()
session = aiohttp_client.async_get_clientsession(self.hass)
self.bridges = await async_discovery(session)
if len(self.bridges) == 1:
self.deconz_config = self.bridges[0]
return await self.async_step_link()
elif len(self.bridges) > 1:
hosts = []
for bridge in self.bridges:
hosts.append(bridge[CONF_HOST])
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({
vol.Required(CONF_HOST): vol.In(hosts)
})
)
return self.async_abort(
reason='no_bridges'
)
async def async_step_link(self, user_input=None):
"""Attempt to link with the deCONZ bridge."""
from pydeconz.utils import async_get_api_key
errors = {}
if user_input is not None:
session = aiohttp_client.async_get_clientsession(self.hass)
api_key = await async_get_api_key(session, **self.deconz_config)
if api_key:
self.deconz_config[CONF_API_KEY] = api_key
return self.async_create_entry(
title='deCONZ',
data=self.deconz_config
)
else:
errors['base'] = 'no_key'
return self.async_show_form(
step_id='link',
errors=errors,
)
async def async_setup_entry(hass, entry):
"""Set up a bridge for a config entry."""
if DOMAIN in hass.data:
_LOGGER.error(
"Config entry failed since one deCONZ instance already exists")
return False
result = await async_setup_deconz(hass, None, entry.data)
if result:
return True
return False

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"
},
"abort": {
"already_configured": "Bridge is already configured",
"no_bridges": "No deCONZ bridges discovered",
"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)
def get_extra_attributes(self, device: str) -> dict:
"""Get the extra attributes of a device."""
raise NotImplementedError()
def async_get_extra_attributes(self, device: str) -> Any:
"""Get the extra attributes of a device.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.get_extra_attributes, device)
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
"""Load devices from YAML configuration file."""
@ -690,10 +701,20 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
host_name = yield from scanner.async_get_device_name(mac)
seen.add(mac)
try:
extra_attributes = (yield from
scanner.async_get_extra_attributes(mac))
except NotImplementedError:
extra_attributes = dict()
kwargs = {
'mac': mac,
'host_name': host_name,
'source_type': SOURCE_TYPE_ROUTER
'source_type': SOURCE_TYPE_ROUTER,
'attributes': {
'scanner': scanner.__class__.__name__,
**extra_attributes
}
}
zone_home = hass.states.get(zone.ENTITY_ID_HOME)

View file

@ -48,8 +48,11 @@ class BMWDeviceTracker(object):
return
_LOGGER.debug('Updating %s', dev_id)
attrs = {
'vin': self.vehicle.vin,
}
self._see(
dev_id=dev_id, host_name=self.vehicle.name,
gps=self.vehicle.state.gps_position, icon='mdi:car'
gps=self.vehicle.state.gps_position, attributes=attrs,
icon='mdi:car'
)

View file

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

View file

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

View file

@ -80,6 +80,8 @@ class NmapDeviceScanner(DeviceScanner):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
_LOGGER.debug("Nmap last results %s", self.last_results)
return [device.mac for device in self.last_results]
def get_device_name(self, device):
@ -91,6 +93,13 @@ class NmapDeviceScanner(DeviceScanner):
return filter_named[0]
return None
def get_extra_attributes(self, device):
"""Return the IP of the given device."""
filter_ip = next((
result.ip for result in self.last_results
if result.mac == device), None)
return {'ip': filter_ip}
def _update_info(self):
"""Scan the network for devices.

View file

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

View file

@ -122,3 +122,9 @@ class UnifiScanner(DeviceScanner):
name = client.get('name') or client.get('hostname')
_LOGGER.debug("Device mac %s name %s", device, name)
return name
def get_extra_attributes(self, device):
"""Return the extra attributes of the device."""
client = self._clients.get(device, {})
_LOGGER.debug("Device mac %s attributes %s", device, client)
return client

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)),
})
REQUIREMENTS = ['python-miio==0.3.9']
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
def get_scanner(hass, config):
@ -41,7 +41,7 @@ def get_scanner(hass, config):
device_info.model,
device_info.firmware_version,
device_info.hardware_version)
scanner = XiaomiMiioDeviceScanner(hass, device)
scanner = XiaomiMiioDeviceScanner(device)
except DeviceException as ex:
_LOGGER.error("Device unavailable or token incorrect: %s", ex)
@ -51,7 +51,7 @@ def get_scanner(hass, config):
class XiaomiMiioDeviceScanner(DeviceScanner):
"""This class queries a Xiaomi Mi WiFi Repeater."""
def __init__(self, hass, device):
def __init__(self, device):
"""Initialize the scanner."""
self.device = device

View file

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

View file

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

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.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20180404.0']
REQUIREMENTS = ['home-assistant-frontend==20180420.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']

View file

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

View file

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

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)
def get_last_state_changes(hass, number_of_states, entity_id):
"""Return the last number_of_states."""
from homeassistant.components.recorder.models import States
start_time = dt_util.utcnow()
with session_scope(hass=hass) as session:
query = session.query(States).filter(
(States.last_changed == States.last_updated))
if entity_id is not None:
query = query.filter_by(entity_id=entity_id.lower())
entity_ids = [entity_id] if entity_id is not None else None
states = execute(
query.order_by(States.last_updated.desc()).limit(number_of_states))
return states_to_json(hass, reversed(states),
start_time,
entity_ids,
include_start_time_state=False)
def get_states(hass, utc_point_in_time, entity_ids=None, run=None,
filters=None):
"""Return the states at a specific point in time."""

View file

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

View file

@ -8,12 +8,11 @@ from zlib import adler32
import voluptuous as vol
from homeassistant.components.climate import (
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.components.cover import SUPPORT_SET_POSITION
from homeassistant.components.cover import (
SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION)
from homeassistant.const import (
ATTR_CODE, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
ATTR_DEVICE_CLASS, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
@ -21,14 +20,16 @@ from homeassistant.util import get_local_ip
from homeassistant.util.decorator import Registry
from .const import (
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER,
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START)
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START,
DEVICE_CLASS_CO2, DEVICE_CLASS_LIGHT, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PM25, DEVICE_CLASS_TEMPERATURE)
from .util import (
validate_entity_config, show_setup_message)
TYPES = Registry()
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['HAP-python==1.1.8']
REQUIREMENTS = ['HAP-python==1.1.9']
CONFIG_SCHEMA = vol.Schema({
@ -79,55 +80,64 @@ def get_accessory(hass, state, aid, config):
state.entity_id)
return None
if state.domain == 'sensor':
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT:
_LOGGER.debug('Add "%s" as "%s"',
state.entity_id, 'TemperatureSensor')
return TYPES['TemperatureSensor'](hass, state.entity_id,
state.name, aid=aid)
elif unit == '%':
_LOGGER.debug('Add "%s" as %s"',
state.entity_id, 'HumiditySensor')
return TYPES['HumiditySensor'](hass, state.entity_id, state.name,
aid=aid)
a_type = None
config = config or {}
elif state.domain == 'cover':
# Only add covers that support set_cover_position
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if features & SUPPORT_SET_POSITION:
_LOGGER.debug('Add "%s" as "%s"',
state.entity_id, 'WindowCovering')
return TYPES['WindowCovering'](hass, state.entity_id, state.name,
aid=aid)
if state.domain == 'alarm_control_panel':
a_type = 'SecuritySystem'
elif state.domain == 'alarm_control_panel':
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem')
return TYPES['SecuritySystem'](hass, state.entity_id, state.name,
alarm_code=config.get(ATTR_CODE),
aid=aid)
elif state.domain == 'binary_sensor' or state.domain == 'device_tracker':
a_type = 'BinarySensor'
elif state.domain == 'climate':
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \
SUPPORT_TARGET_TEMPERATURE_HIGH
# Check if climate device supports auto mode
support_auto = bool(features & support_temp_range)
a_type = 'Thermostat'
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat')
return TYPES['Thermostat'](hass, state.entity_id,
state.name, support_auto, aid=aid)
elif state.domain == 'cover':
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
if device_class == 'garage' and \
features & (SUPPORT_OPEN | SUPPORT_CLOSE):
a_type = 'GarageDoorOpener'
elif features & SUPPORT_SET_POSITION:
a_type = 'WindowCovering'
elif features & (SUPPORT_OPEN | SUPPORT_CLOSE):
a_type = 'WindowCoveringBasic'
elif state.domain == 'light':
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light')
return TYPES['Light'](hass, state.entity_id, state.name, aid=aid)
a_type = 'Light'
elif state.domain == 'lock':
a_type = 'Lock'
elif state.domain == 'sensor':
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
if device_class == DEVICE_CLASS_TEMPERATURE or unit == TEMP_CELSIUS \
or unit == TEMP_FAHRENHEIT:
a_type = 'TemperatureSensor'
elif device_class == DEVICE_CLASS_HUMIDITY or unit == '%':
a_type = 'HumiditySensor'
elif device_class == DEVICE_CLASS_PM25 \
or DEVICE_CLASS_PM25 in state.entity_id:
a_type = 'AirQualitySensor'
elif device_class == DEVICE_CLASS_CO2 \
or DEVICE_CLASS_CO2 in state.entity_id:
a_type = 'CarbonDioxideSensor'
elif device_class == DEVICE_CLASS_LIGHT or unit == 'lm' or \
unit == 'lux':
a_type = 'LightSensor'
elif state.domain == 'switch' or state.domain == 'remote' \
or state.domain == 'input_boolean' or state.domain == 'script':
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch')
return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid)
a_type = 'Switch'
return None
if a_type is None:
return None
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type)
return TYPES[a_type](hass, state.name, state.entity_id, aid, config=config)
def generate_aid(entity_id):
@ -143,7 +153,7 @@ class HomeKit():
def __init__(self, hass, port, entity_filter, entity_config):
"""Initialize a HomeKit object."""
self._hass = hass
self.hass = hass
self._port = port
self._filter = entity_filter
self._config = entity_config
@ -156,11 +166,11 @@ class HomeKit():
"""Setup bridge and accessory driver."""
from .accessories import HomeBridge, HomeDriver
self._hass.bus.async_listen_once(
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, self.stop)
path = self._hass.config.path(HOMEKIT_FILE)
self.bridge = HomeBridge(self._hass)
path = self.hass.config.path(HOMEKIT_FILE)
self.bridge = HomeBridge(self.hass)
self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path)
def add_bridge_accessory(self, state):
@ -169,7 +179,7 @@ class HomeKit():
return
aid = generate_aid(state.entity_id)
conf = self._config.pop(state.entity_id, {})
acc = get_accessory(self._hass, state, aid, conf)
acc = get_accessory(self.hass, state, aid, conf)
if acc is not None:
self.bridge.add_accessory(acc)
@ -181,15 +191,15 @@ class HomeKit():
# pylint: disable=unused-variable
from . import ( # noqa F401
type_covers, type_lights, type_security_systems, type_sensors,
type_switches, type_thermostats)
type_covers, type_lights, type_locks, type_security_systems,
type_sensors, type_switches, type_thermostats)
for state in self._hass.states.all():
for state in self.hass.states.all():
self.add_bridge_accessory(state)
self.bridge.set_broker(self.driver)
if not self.bridge.paired:
show_setup_message(self.bridge, self._hass)
show_setup_message(self.hass, self.bridge)
_LOGGER.debug('Driver start')
self.driver.start()

View file

@ -1,21 +1,64 @@
"""Extend the basic Accessory and Bridge functions."""
from datetime import timedelta
from functools import wraps
from inspect import getmodule
import logging
from pyhap.accessory import Accessory, Bridge, Category
from pyhap.accessory_driver import AccessoryDriver
from homeassistant.helpers.event import async_track_state_change
from homeassistant.core import callback as ha_callback
from homeassistant.helpers.event import (
async_track_state_change, track_point_in_utc_time)
from homeassistant.util import dt as dt_util
from .const import (
ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL,
CHAR_NAME, CHAR_SERIAL_NUMBER)
DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER,
SERV_ACCESSORY_INFO, CHAR_MANUFACTURER,
CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
from .util import (
show_setup_message, dismiss_setup_message)
_LOGGER = logging.getLogger(__name__)
def debounce(func):
"""Decorator function. Debounce callbacks form HomeKit."""
@ha_callback
def call_later_listener(*args):
"""Callback listener called from call_later."""
# pylint: disable=unsubscriptable-object
nonlocal lastargs, remove_listener
hass = lastargs['hass']
hass.async_add_job(func, *lastargs['args'])
lastargs = remove_listener = None
@wraps(func)
def wrapper(*args):
"""Wrapper starts async timer.
The accessory must have 'self.hass' and 'self.entity_id' as attributes.
"""
# pylint: disable=not-callable
hass = args[0].hass
nonlocal lastargs, remove_listener
if remove_listener:
remove_listener()
lastargs = remove_listener = None
lastargs = {'hass': hass, 'args': [*args]}
remove_listener = track_point_in_utc_time(
hass, call_later_listener,
dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT))
logger.debug('%s: Start %s timeout', args[0].entity_id,
func.__name__.replace('set_', ''))
remove_listener = None
lastargs = None
name = getmodule(func).__name__
logger = logging.getLogger(name)
return wrapper
def add_preload_service(acc, service, chars=None):
"""Define and return a service to be available for the accessory."""
from pyhap.loader import get_serv_loader, get_char_loader
@ -29,6 +72,18 @@ def add_preload_service(acc, service, chars=None):
return service
def setup_char(char_name, service, value=None, properties=None, callback=None):
"""Helper function to return fully configured characteristic."""
char = service.get_characteristic(char_name)
if value:
char.value = value
if properties:
char.override_properties(properties)
if callback:
char.setter_callback = callback
return char
def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
serial_number='0000'):
"""Set the default accessory information."""
@ -42,14 +97,13 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
class HomeAccessory(Accessory):
"""Adapter class for Accessory."""
# pylint: disable=no-member
def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL,
category='OTHER', **kwargs):
def __init__(self, hass, name, entity_id, aid, category):
"""Initialize a Accessory object."""
super().__init__(name, **kwargs)
set_accessory_info(self, name, model)
super().__init__(name, aid=aid)
set_accessory_info(self, name, model=entity_id)
self.category = getattr(Category, category, Category.OTHER)
self.entity_id = entity_id
self.hass = hass
def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO)
@ -57,19 +111,33 @@ class HomeAccessory(Accessory):
def run(self):
"""Method called by accessory after driver is started."""
state = self.hass.states.get(self.entity_id)
self.update_state(new_state=state)
self.update_state_callback(new_state=state)
async_track_state_change(
self.hass, self.entity_id, self.update_state)
self.hass, self.entity_id, self.update_state_callback)
def update_state_callback(self, entity_id=None, old_state=None,
new_state=None):
"""Callback from state change listener."""
_LOGGER.debug('New_state: %s', new_state)
if new_state is None:
return
self.update_state(new_state)
def update_state(self, new_state):
"""Method called on state change to update HomeKit value.
Overridden by accessory types.
"""
pass
class HomeBridge(Bridge):
"""Adapter class for Bridge."""
def __init__(self, hass, name=BRIDGE_NAME,
model=BRIDGE_MODEL, **kwargs):
def __init__(self, hass, name=BRIDGE_NAME):
"""Initialize a Bridge object."""
super().__init__(name, **kwargs)
set_accessory_info(self, name, model)
super().__init__(name)
set_accessory_info(self, name, model=BRIDGE_MODEL)
self.hass = hass
def _set_services(self):
@ -87,7 +155,7 @@ class HomeBridge(Bridge):
def remove_paired_client(self, client_uuid):
"""Override super function to show setup message if unpaired."""
super().remove_paired_client(client_uuid)
show_setup_message(self, self.hass)
show_setup_message(self.hass, self)
class HomeDriver(AccessoryDriver):

View file

@ -1,5 +1,6 @@
"""Constants used be the HomeKit component."""
# #### MISC ####
DEBOUNCE_TIMEOUT = 0.5
DOMAIN = 'homekit'
HOMEKIT_FILE = '.homekit.state'
HOMEKIT_NOTIFY_ID = 4663548
@ -17,15 +18,15 @@ DEFAULT_PORT = 51827
SERVICE_HOMEKIT_START = 'start'
# #### STRING CONSTANTS ####
ACCESSORY_MODEL = 'homekit.accessory'
ACCESSORY_NAME = 'Home Accessory'
BRIDGE_MODEL = 'homekit.bridge'
BRIDGE_NAME = 'Home Assistant'
MANUFACTURER = 'HomeAssistant'
# #### Categories ####
CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM'
CATEGORY_GARAGE_DOOR_OPENER = 'GARAGE_DOOR_OPENER'
CATEGORY_LIGHT = 'LIGHTBULB'
CATEGORY_LOCK = 'DOOR_LOCK'
CATEGORY_SENSOR = 'SENSOR'
CATEGORY_SWITCH = 'SWITCH'
CATEGORY_THERMOSTAT = 'THERMOSTAT'
@ -34,40 +35,80 @@ CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING'
# #### Services ####
SERV_ACCESSORY_INFO = 'AccessoryInformation'
SERV_HUMIDITY_SENSOR = 'HumiditySensor'
# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered,
# StatusLowBattery, Name
SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor'
SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor'
SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor'
SERV_CONTACT_SENSOR = 'ContactSensor'
SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener'
SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity
SERV_LEAK_SENSOR = 'LeakSensor'
SERV_LIGHT_SENSOR = 'LightSensor'
SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name
SERV_LOCK = 'LockMechanism'
SERV_MOTION_SENSOR = 'MotionSensor'
SERV_OCCUPANCY_SENSOR = 'OccupancySensor'
SERV_SECURITY_SYSTEM = 'SecuritySystem'
SERV_SMOKE_SENSOR = 'SmokeSensor'
SERV_SWITCH = 'Switch'
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
SERV_THERMOSTAT = 'Thermostat'
SERV_WINDOW_COVERING = 'WindowCovering'
# CurrentPosition, TargetPosition, PositionState
# #### Characteristics ####
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
CHAR_AIR_QUALITY = 'AirQuality'
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100]
CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected'
CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel'
CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel'
CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected'
CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState'
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel'
CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState'
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
CHAR_CURRENT_POSITION = 'CurrentPosition'
CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100]
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
CHAR_LEAK_DETECTED = 'LeakDetected'
CHAR_LOCK_CURRENT_STATE = 'LockCurrentState'
CHAR_LOCK_TARGET_STATE = 'LockTargetState'
CHAR_LINK_QUALITY = 'LinkQuality'
CHAR_MANUFACTURER = 'Manufacturer'
CHAR_MODEL = 'Model'
CHAR_MOTION_DETECTED = 'MotionDetected'
CHAR_NAME = 'Name'
CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected'
CHAR_ON = 'On' # boolean
CHAR_POSITION_STATE = 'PositionState'
CHAR_SATURATION = 'Saturation' # percent
CHAR_SERIAL_NUMBER = 'SerialNumber'
CHAR_SMOKE_DETECTED = 'SmokeDetected'
CHAR_TARGET_DOOR_STATE = 'TargetDoorState'
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
CHAR_TARGET_POSITION = 'TargetPosition'
CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100]
CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
# #### Properties ####
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}
# #### Device Class ####
DEVICE_CLASS_CO2 = 'co2'
DEVICE_CLASS_GAS = 'gas'
DEVICE_CLASS_HUMIDITY = 'humidity'
DEVICE_CLASS_LIGHT = 'light'
DEVICE_CLASS_MOISTURE = 'moisture'
DEVICE_CLASS_MOTION = 'motion'
DEVICE_CLASS_OCCUPANCY = 'occupancy'
DEVICE_CLASS_OPENING = 'opening'
DEVICE_CLASS_PM25 = 'pm25'
DEVICE_CLASS_SMOKE = 'smoke'
DEVICE_CLASS_TEMPERATURE = 'temperature'

View file

@ -1,18 +1,67 @@
"""Class to hold all cover accessories."""
import logging
from homeassistant.components.cover import ATTR_CURRENT_POSITION
from homeassistant.components.cover import (
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP)
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED,
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_STOP_COVER,
ATTR_SUPPORTED_FEATURES)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import (
CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING,
CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE,
CATEGORY_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER,
CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE)
_LOGGER = logging.getLogger(__name__)
@TYPES.register('GarageDoorOpener')
class GarageDoorOpener(HomeAccessory):
"""Generate a Garage Door Opener accessory for a cover entity.
The cover entity must be in the 'garage' device class
and support no more than open, close, and stop.
"""
def __init__(self, *args, config):
"""Initialize a GarageDoorOpener accessory object."""
super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER)
self.flag_target_state = False
serv_garage_door = add_preload_service(self, SERV_GARAGE_DOOR_OPENER)
self.char_current_state = setup_char(
CHAR_CURRENT_DOOR_STATE, serv_garage_door, value=0)
self.char_target_state = setup_char(
CHAR_TARGET_DOOR_STATE, serv_garage_door, value=0,
callback=self.set_state)
def set_state(self, value):
"""Change garage state if call came from HomeKit."""
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
self.flag_target_state = True
if value == 0:
self.char_current_state.set_value(3)
self.hass.components.cover.open_cover(self.entity_id)
elif value == 1:
self.char_current_state.set_value(2)
self.hass.components.cover.close_cover(self.entity_id)
def update_state(self, new_state):
"""Update cover state after state changed."""
hass_state = new_state.state
if hass_state in (STATE_OPEN, STATE_CLOSED):
current_state = 0 if hass_state == STATE_OPEN else 1
self.char_current_state.set_value(current_state)
if not self.flag_target_state:
self.char_target_state.set_value(current_state)
self.flag_target_state = False
@TYPES.register('WindowCovering')
class WindowCovering(HomeAccessory):
"""Generate a Window accessory for a cover entity.
@ -20,54 +69,91 @@ class WindowCovering(HomeAccessory):
The cover entity must support: set_cover_position.
"""
def __init__(self, hass, entity_id, display_name, **kwargs):
def __init__(self, *args, config):
"""Initialize a WindowCovering accessory object."""
super().__init__(display_name, entity_id,
CATEGORY_WINDOW_COVERING, **kwargs)
self.hass = hass
self.entity_id = entity_id
self.current_position = None
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
self.homekit_target = None
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = serv_cover. \
get_characteristic(CHAR_CURRENT_POSITION)
self.char_target_position = serv_cover. \
get_characteristic(CHAR_TARGET_POSITION)
self.char_position_state = serv_cover. \
get_characteristic(CHAR_POSITION_STATE)
self.char_current_position.value = 0
self.char_target_position.value = 0
self.char_position_state.value = 0
self.char_target_position.setter_callback = self.move_cover
self.char_current_position = setup_char(
CHAR_CURRENT_POSITION, serv_cover, value=0)
self.char_target_position = setup_char(
CHAR_TARGET_POSITION, serv_cover, value=0,
callback=self.move_cover)
def move_cover(self, value):
"""Move cover to value if call came from HomeKit."""
self.char_target_position.set_value(value, should_callback=False)
if value != self.current_position:
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
self.homekit_target = value
if value > self.current_position:
self.char_position_state.set_value(1)
elif value < self.current_position:
self.char_position_state.set_value(0)
self.hass.components.cover.set_cover_position(
value, self.entity_id)
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
self.homekit_target = value
def update_state(self, entity_id=None, old_state=None, new_state=None):
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_POSITION: value}
self.hass.services.call(DOMAIN, SERVICE_SET_COVER_POSITION, params)
def update_state(self, new_state):
"""Update cover position after state changed."""
if new_state is None:
return
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
if isinstance(current_position, int):
self.current_position = current_position
self.char_current_position.set_value(self.current_position)
self.char_current_position.set_value(current_position)
if self.homekit_target is None or \
abs(self.current_position - self.homekit_target) < 6:
self.char_target_position.set_value(self.current_position)
self.char_position_state.set_value(2)
abs(current_position - self.homekit_target) < 6:
self.char_target_position.set_value(current_position)
self.homekit_target = None
@TYPES.register('WindowCoveringBasic')
class WindowCoveringBasic(HomeAccessory):
"""Generate a Window accessory for a cover entity.
The cover entity must support: open_cover, close_cover,
stop_cover (optional).
"""
def __init__(self, *args, config):
"""Initialize a WindowCovering accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
features = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_SUPPORTED_FEATURES)
self.supports_stop = features & SUPPORT_STOP
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = setup_char(
CHAR_CURRENT_POSITION, serv_cover, value=0)
self.char_target_position = setup_char(
CHAR_TARGET_POSITION, serv_cover, value=0,
callback=self.move_cover)
self.char_position_state = setup_char(
CHAR_POSITION_STATE, serv_cover, value=2)
def move_cover(self, value):
"""Move cover to value if call came from HomeKit."""
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
if self.supports_stop:
if value > 70:
service, position = (SERVICE_OPEN_COVER, 100)
elif value < 30:
service, position = (SERVICE_CLOSE_COVER, 0)
else:
service, position = (SERVICE_STOP_COVER, 50)
else:
if value >= 50:
service, position = (SERVICE_OPEN_COVER, 100)
else:
service, position = (SERVICE_CLOSE_COVER, 0)
self.hass.services.call(DOMAIN, service,
{ATTR_ENTITY_ID: self.entity_id})
# Snap the current/target position to the expected final position.
self.char_current_position.set_value(position)
self.char_target_position.set_value(position)
self.char_position_state.set_value(2)
def update_state(self, new_state):
"""Update cover position after state changed."""
position_mapping = {STATE_OPEN: 100, STATE_CLOSED: 0}
hk_position = position_mapping.get(new_state.state)
if hk_position is not None:
self.char_current_position.set_value(hk_position)
self.char_target_position.set_value(hk_position)
self.char_position_state.set_value(2)

View file

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

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

View file

@ -2,18 +2,41 @@
import logging
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS,
ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import (
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
from .util import convert_to_float, temperature_to_homekit
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS,
SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY,
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL,
SERV_LIGHT_SENSOR, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL,
DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED,
DEVICE_CLASS_GAS, SERV_CARBON_MONOXIDE_SENSOR,
CHAR_CARBON_MONOXIDE_DETECTED,
DEVICE_CLASS_MOISTURE, SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED,
DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED,
DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED,
DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE,
DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED)
from .util import (
convert_to_float, temperature_to_homekit, density_to_air_quality)
_LOGGER = logging.getLogger(__name__)
BINARY_SENSOR_SERVICE_MAP = {
DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR,
CHAR_CARBON_DIOXIDE_DETECTED),
DEVICE_CLASS_GAS: (SERV_CARBON_MONOXIDE_SENSOR,
CHAR_CARBON_MONOXIDE_DETECTED),
DEVICE_CLASS_MOISTURE: (SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED),
DEVICE_CLASS_MOTION: (SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED),
DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED),
DEVICE_CLASS_OPENING: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE),
DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED)}
@TYPES.register('TemperatureSensor')
class TemperatureSensor(HomeAccessory):
@ -22,29 +45,22 @@ class TemperatureSensor(HomeAccessory):
Sensor entity must return temperature in °C, °F.
"""
def __init__(self, hass, entity_id, name, **kwargs):
def __init__(self, *args, config):
"""Initialize a TemperatureSensor accessory object."""
super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs)
self.hass = hass
self.entity_id = entity_id
super().__init__(*args, category=CATEGORY_SENSOR)
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE)
self.char_temp.override_properties(properties=PROP_CELSIUS)
self.char_temp.value = 0
self.char_temp = setup_char(
CHAR_CURRENT_TEMPERATURE, serv_temp, value=0,
properties=PROP_CELSIUS)
self.unit = None
def update_state(self, entity_id=None, old_state=None, new_state=None):
def update_state(self, new_state):
"""Update temperature after state changed."""
if new_state is None:
return
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
temperature = convert_to_float(new_state.state)
if temperature:
temperature = temperature_to_homekit(temperature, unit)
self.char_temp.set_value(temperature, should_callback=False)
self.char_temp.set_value(temperature)
_LOGGER.debug('%s: Current temperature set to %d°C',
self.entity_id, temperature)
@ -53,25 +69,113 @@ class TemperatureSensor(HomeAccessory):
class HumiditySensor(HomeAccessory):
"""Generate a HumiditySensor accessory as humidity sensor."""
def __init__(self, hass, entity_id, name, *args, **kwargs):
def __init__(self, *args, config):
"""Initialize a HumiditySensor accessory object."""
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
self.hass = hass
self.entity_id = entity_id
super().__init__(*args, category=CATEGORY_SENSOR)
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
self.char_humidity = serv_humidity \
.get_characteristic(CHAR_CURRENT_HUMIDITY)
self.char_humidity.value = 0
self.char_humidity = setup_char(
CHAR_CURRENT_HUMIDITY, serv_humidity, value=0)
def update_state(self, entity_id=None, old_state=None, new_state=None):
def update_state(self, new_state):
"""Update accessory after state change."""
if new_state is None:
return
humidity = convert_to_float(new_state.state)
if humidity:
self.char_humidity.set_value(humidity, should_callback=False)
self.char_humidity.set_value(humidity)
_LOGGER.debug('%s: Percent set to %d%%',
self.entity_id, humidity)
@TYPES.register('AirQualitySensor')
class AirQualitySensor(HomeAccessory):
"""Generate a AirQualitySensor accessory as air quality sensor."""
def __init__(self, *args, config):
"""Initialize a AirQualitySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
serv_air_quality = add_preload_service(self, SERV_AIR_QUALITY_SENSOR,
[CHAR_AIR_PARTICULATE_DENSITY])
self.char_quality = setup_char(
CHAR_AIR_QUALITY, serv_air_quality, value=0)
self.char_density = setup_char(
CHAR_AIR_PARTICULATE_DENSITY, serv_air_quality, value=0)
def update_state(self, new_state):
"""Update accessory after state change."""
density = convert_to_float(new_state.state)
if density is not None:
self.char_density.set_value(density)
self.char_quality.set_value(density_to_air_quality(density))
_LOGGER.debug('%s: Set to %d', self.entity_id, density)
@TYPES.register('CarbonDioxideSensor')
class CarbonDioxideSensor(HomeAccessory):
"""Generate a CarbonDioxideSensor accessory as CO2 sensor."""
def __init__(self, *args, config):
"""Initialize a CarbonDioxideSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
serv_co2 = add_preload_service(self, SERV_CARBON_DIOXIDE_SENSOR, [
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL])
self.char_co2 = setup_char(
CHAR_CARBON_DIOXIDE_LEVEL, serv_co2, value=0)
self.char_peak = setup_char(
CHAR_CARBON_DIOXIDE_PEAK_LEVEL, serv_co2, value=0)
self.char_detected = setup_char(
CHAR_CARBON_DIOXIDE_DETECTED, serv_co2, value=0)
def update_state(self, new_state):
"""Update accessory after state change."""
co2 = convert_to_float(new_state.state)
if co2 is not None:
self.char_co2.set_value(co2)
if co2 > self.char_peak.value:
self.char_peak.set_value(co2)
self.char_detected.set_value(co2 > 1000)
_LOGGER.debug('%s: Set to %d', self.entity_id, co2)
@TYPES.register('LightSensor')
class LightSensor(HomeAccessory):
"""Generate a LightSensor accessory as light sensor."""
def __init__(self, *args, config):
"""Initialize a LightSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
serv_light = add_preload_service(self, SERV_LIGHT_SENSOR)
self.char_light = setup_char(
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, serv_light, value=0)
def update_state(self, new_state):
"""Update accessory after state change."""
luminance = convert_to_float(new_state.state)
if luminance is not None:
self.char_light.set_value(luminance)
_LOGGER.debug('%s: Set to %d', self.entity_id, luminance)
@TYPES.register('BinarySensor')
class BinarySensor(HomeAccessory):
"""Generate a BinarySensor accessory as binary sensor."""
def __init__(self, *args, config):
"""Initialize a BinarySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
device_class = self.hass.states.get(self.entity_id).attributes \
.get(ATTR_DEVICE_CLASS)
service_char = BINARY_SENSOR_SERVICE_MAP[device_class] \
if device_class in BINARY_SENSOR_SERVICE_MAP \
else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY]
service = add_preload_service(self, service_char[0])
self.char_detected = setup_char(service_char[1], service, value=0)
def update_state(self, new_state):
"""Update accessory after state change."""
state = new_state.state
detected = (state == STATE_ON) or (state == STATE_HOME)
self.char_detected.set_value(detected)
_LOGGER.debug('%s: Set to %d', self.entity_id, detected)

View file

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

View file

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

View file

@ -33,7 +33,7 @@ def validate_entity_config(values):
return entities
def show_setup_message(bridge, hass):
def show_setup_message(hass, bridge):
"""Display persistent notification with setup information."""
pin = bridge.pincode.decode()
_LOGGER.info('Pincode: %s', pin)
@ -64,3 +64,16 @@ def temperature_to_homekit(temperature, unit):
def temperature_to_states(temperature, unit):
"""Convert temperature back from Celsius to Home Assistant unit."""
return round(temp_util.convert(temperature, TEMP_CELSIUS, unit), 1)
def density_to_air_quality(density):
"""Map PM2.5 density to HomeKit AirQuality level."""
if density <= 35:
return 1
elif density <= 75:
return 2
elif density <= 115:
return 3
elif density <= 150:
return 4
return 5

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

View file

@ -131,3 +131,9 @@ async def async_setup_entry(hass, entry):
bridge = HueBridge(hass, entry, allow_unreachable, allow_groups)
hass.data[DOMAIN][host] = bridge
return await bridge.async_setup()
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
bridge = hass.data[DOMAIN].pop(entry.data['host'])
return await bridge.async_reset()

View file

@ -30,6 +30,7 @@ class HueBridge(object):
self.allow_groups = allow_groups
self.available = True
self.api = None
self._cancel_retry_setup = None
@property
def host(self):
@ -39,18 +40,17 @@ class HueBridge(object):
async def async_setup(self, tries=0):
"""Set up a phue bridge based on host parameter."""
host = self.host
hass = self.hass
try:
self.api = await get_bridge(
self.hass, host,
self.config_entry.data['username']
)
hass, host, self.config_entry.data['username'])
except AuthenticationRequired:
# usernames can become invalid if hub is reset or user removed.
# We are going to fail the config entry setup and initiate a new
# linking procedure. When linking succeeds, it will remove the
# old config entry.
self.hass.async_add_job(self.hass.config_entries.flow.async_init(
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data={
'host': host,
}
@ -68,8 +68,8 @@ class HueBridge(object):
# This feels hacky, we should find a better way to do this
self.config_entry.state = config_entries.ENTRY_STATE_LOADED
# Unhandled edge case: cancel this if we discover bridge on new IP
self.hass.helpers.event.async_call_later(retry_delay, retry_setup)
self._cancel_retry_setup = hass.helpers.event.async_call_later(
retry_delay, retry_setup)
return False
@ -78,16 +78,43 @@ class HueBridge(object):
host)
return False
self.hass.async_add_job(
self.hass.helpers.discovery.async_load_platform(
'light', DOMAIN, {'host': host}))
hass.async_add_job(hass.config_entries.async_forward_entry_setup(
self.config_entry, 'light'))
self.hass.services.async_register(
hass.services.async_register(
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
schema=SCENE_SCHEMA)
return True
async def async_reset(self):
"""Reset this bridge to default state.
Will cancel any scheduled setup retry and will unload
the config entry.
"""
# The bridge can be in 3 states:
# - Setup was successful, self.api is not None
# - Authentication was wrong, self.api is None, not retrying setup.
# - Host was down. self.api is None, we're retrying setup
# If we have a retry scheduled, we were never setup.
if self._cancel_retry_setup is not None:
self._cancel_retry_setup()
self._cancel_retry_setup = None
return True
# If the authentication was wrong.
if self.api is None:
return True
self.hass.services.async_remove(DOMAIN, SERVICE_HUE_SCENE)
# If setup was successful, we set api variable, forwarded entry and
# register service
return await self.hass.config_entries.async_forward_entry_unload(
self.config_entry, 'light')
async def hue_activate_scene(self, call, updated=False):
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]

View file

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

View file

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

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

View file

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

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/
"""
import logging
from math import ceil
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_HS_COLOR,
@ -51,7 +51,9 @@ class AbodeLight(AbodeDevice, Light):
*kwargs[ATTR_HS_COLOR]))
if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable:
self._device.set_level(kwargs[ATTR_BRIGHTNESS])
# Convert HASS brightness (0-255) to Abode brightness (0-99)
# If 100 is sent to Abode, response is 99 causing an error
self._device.set_level(ceil(kwargs[ATTR_BRIGHTNESS] * 99 / 255.0))
else:
self._device.switch_on()
@ -68,7 +70,12 @@ class AbodeLight(AbodeDevice, Light):
def brightness(self):
"""Return the brightness of the light."""
if self._device.is_dimmable and self._device.has_brightness:
return self._device.brightness
brightness = int(self._device.brightness)
# Abode returns 100 during device initialization and device refresh
if brightness == 100:
return 255
# Convert Abode brightness (0-99) to HASS brightness (0-255)
return ceil(brightness * 255 / 99.0)
@property
def hs_color(self):

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

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,
discovery_info=None):
"""Set up the Hue lights."""
if discovery_info is None:
return
"""Old way of setting up Hue lights.
bridge = hass.data[hue.DOMAIN][discovery_info['host']]
Can only be called when a user accidentally mentions hue platform in their
config. But even in that case it would have been ignored.
"""
pass
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up the Hue lights from a config entry."""
bridge = hass.data[hue.DOMAIN][config_entry.data['host']]
cur_lights = {}
cur_groups = {}

View file

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

View file

@ -27,9 +27,9 @@ class QSLight(QSToggleEntity, Light):
@property
def brightness(self):
"""Return the brightness of this light (0-255)."""
return self._qsusb[self.qsid, 1] if self._dim else None
return self.device.value if self.device.is_dimmer else None
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_BRIGHTNESS if self._dim else 0
return SUPPORT_BRIGHTNESS if self.device.is_dimmer else 0

View file

@ -24,6 +24,14 @@ REQUIREMENTS = ['yeelight==0.4.0']
_LOGGER = logging.getLogger(__name__)
LEGACY_DEVICE_TYPE_MAP = {
'color1': 'rgb',
'mono1': 'white',
'strip1': 'strip',
'bslamp1': 'bedside',
'ceiling1': 'ceiling',
}
CONF_TRANSITION = 'transition'
DEFAULT_TRANSITION = 350
@ -122,8 +130,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is not None:
_LOGGER.debug("Adding autodetected %s", discovery_info['hostname'])
device_type = discovery_info['device_type']
device_type = LEGACY_DEVICE_TYPE_MAP.get(device_type, device_type)
# Not using hostname, as it seems to vary.
name = "yeelight_%s_%s" % (discovery_info['device_type'],
name = "yeelight_%s_%s" % (device_type,
discovery_info['properties']['mac'])
device = {'name': name, 'ipaddr': discovery_info['host']}

View file

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

View file

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

View file

@ -22,12 +22,22 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 62910
DOMAIN = 'maxcube'
MAXCUBE_HANDLE = 'maxcube'
DATA_KEY = 'maxcube'
NOTIFICATION_ID = 'maxcube_notification'
NOTIFICATION_TITLE = 'Max!Cube gateway setup'
CONF_GATEWAYS = 'gateways'
CONFIG_GATEWAY = vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Required(CONF_GATEWAYS, default={}):
vol.All(cv.ensure_list, [CONFIG_GATEWAY])
}),
}, extra=vol.ALLOW_EXTRA)
@ -36,19 +46,31 @@ def setup(hass, config):
"""Establish connection to MAX! Cube."""
from maxcube.connection import MaxCubeConnection
from maxcube.cube import MaxCube
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {}
host = config.get(DOMAIN).get(CONF_HOST)
port = config.get(DOMAIN).get(CONF_PORT)
connection_failed = 0
gateways = config[DOMAIN][CONF_GATEWAYS]
for gateway in gateways:
host = gateway[CONF_HOST]
port = gateway[CONF_PORT]
try:
cube = MaxCube(MaxCubeConnection(host, port))
except timeout:
_LOGGER.error("Connection to Max!Cube could not be established")
cube = None
try:
cube = MaxCube(MaxCubeConnection(host, port))
hass.data[DATA_KEY][host] = MaxCubeHandle(cube)
except timeout as ex:
_LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex))
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart Home Assistant after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
connection_failed += 1
if connection_failed >= len(gateways):
return False
hass.data[MAXCUBE_HANDLE] = MaxCubeHandle(cube)
load_platform(hass, 'climate', DOMAIN)
load_platform(hass, 'binary_sensor', DOMAIN)

View file

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

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

View file

@ -288,7 +288,8 @@ class CastDevice(MediaPlayerDevice):
self._chromecast = None # type: Optional[pychromecast.Chromecast]
self.cast_status = None
self.media_status = None
self.media_status_received = None
self.media_status_position = None
self.media_status_position_received = None
self._available = False # type: bool
self._status_listener = None # type: Optional[CastStatusListener]
@ -361,7 +362,8 @@ class CastDevice(MediaPlayerDevice):
self._chromecast = None
self.cast_status = None
self.media_status = None
self.media_status_received = None
self.media_status_position = None
self.media_status_position_received = None
self._status_listener.invalidate()
self._status_listener = None
@ -388,8 +390,36 @@ class CastDevice(MediaPlayerDevice):
def new_media_status(self, media_status):
"""Handle updates of the media status."""
# Only use media position for playing/paused,
# and for normal playback rate
if (media_status is None or
abs(media_status.playback_rate - 1) > 0.01 or
not (media_status.player_is_playing or
media_status.player_is_paused)):
self.media_status_position = None
self.media_status_position_received = None
else:
# Avoid unnecessary state attribute updates if player_state and
# calculated position stay the same
now = dt_util.utcnow()
do_update = \
(self.media_status is None or
self.media_status_position is None or
self.media_status.player_state != media_status.player_state)
if not do_update:
if media_status.player_is_playing:
elapsed = now - self.media_status_position_received
do_update = abs(media_status.current_time -
(self.media_status_position +
elapsed.total_seconds())) > 1
else:
do_update = \
self.media_status_position != media_status.current_time
if do_update:
self.media_status_position = media_status.current_time
self.media_status_position_received = now
self.media_status = media_status
self.media_status_received = dt_util.utcnow()
self.schedule_update_ha_state()
def new_connection_status(self, connection_status):
@ -595,13 +625,7 @@ class CastDevice(MediaPlayerDevice):
@property
def media_position(self):
"""Position of current playing media in seconds."""
if self.media_status is None or \
not (self.media_status.player_is_playing or
self.media_status.player_is_paused or
self.media_status.player_is_idle):
return None
return self.media_status.current_time
return self.media_status_position
@property
def media_position_updated_at(self):
@ -609,7 +633,7 @@ class CastDevice(MediaPlayerDevice):
Returns value from homeassistant.util.dt.utcnow().
"""
return self.media_status_received
return self.media_status_position_received
@property
def unique_id(self) -> Optional[str]:

View file

@ -8,6 +8,7 @@ import asyncio
from collections import OrderedDict
from functools import wraps
import logging
import socket
import urllib
import re
@ -157,13 +158,29 @@ def _check_deprecated_turn_off(hass, turn_off_action):
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Kodi platform."""
if DATA_KODI not in hass.data:
hass.data[DATA_KODI] = []
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
tcp_port = config.get(CONF_TCP_PORT)
encryption = config.get(CONF_PROXY_SSL)
websocket = config.get(CONF_ENABLE_WEBSOCKET)
hass.data[DATA_KODI] = dict()
# Is this a manual configuration?
if discovery_info is None:
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
tcp_port = config.get(CONF_TCP_PORT)
encryption = config.get(CONF_PROXY_SSL)
websocket = config.get(CONF_ENABLE_WEBSOCKET)
else:
name = "{} ({})".format(DEFAULT_NAME, discovery_info.get('hostname'))
host = discovery_info.get('host')
port = discovery_info.get('port')
tcp_port = DEFAULT_TCP_PORT
encryption = DEFAULT_PROXY_SSL
websocket = DEFAULT_ENABLE_WEBSOCKET
# Only add a device once, so discovered devices do not override manual
# config.
ip_addr = socket.gethostbyname(host)
if ip_addr in hass.data[DATA_KODI]:
return
entity = KodiDevice(
hass,
@ -175,7 +192,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
turn_off_action=config.get(CONF_TURN_OFF_ACTION),
timeout=config.get(CONF_TIMEOUT), websocket=websocket)
hass.data[DATA_KODI].append(entity)
hass.data[DATA_KODI][ip_addr] = entity
async_add_devices([entity], update_before_add=True)
@asyncio.coroutine
@ -189,10 +206,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if key != 'entity_id'}
entity_ids = service.data.get('entity_id')
if entity_ids:
target_players = [player for player in hass.data[DATA_KODI]
target_players = [player
for player in hass.data[DATA_KODI].values()
if player.entity_id in entity_ids]
else:
target_players = hass.data[DATA_KODI]
target_players = hass.data[DATA_KODI].values()
update_tasks = []
for player in target_players:

View file

@ -8,6 +8,7 @@ import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.media_player import (
MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK,
@ -20,11 +21,11 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, STATE_OFF,
CONF_TIMEOUT, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY,
STATE_UNAVAILABLE
STATE_UNAVAILABLE, EVENT_HOMEASSISTANT_STOP
)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pymediaroom==0.6']
REQUIREMENTS = ['pymediaroom==0.6.3']
_LOGGER = logging.getLogger(__name__)
@ -81,12 +82,21 @@ async def async_setup_platform(hass, config, async_add_devices,
if not config[CONF_OPTIMISTIC]:
from pymediaroom import install_mediaroom_protocol
already_installed = hass.data.get(DISCOVERY_MEDIAROOM, False)
already_installed = hass.data.get(DISCOVERY_MEDIAROOM, None)
if not already_installed:
await install_mediaroom_protocol(
hass.data[DISCOVERY_MEDIAROOM] = await install_mediaroom_protocol(
responses_callback=callback_notify)
@callback
def stop_discovery(event):
"""Stop discovery of new mediaroom STB's."""
_LOGGER.debug("Stopping internal pymediaroom discovery.")
hass.data[DISCOVERY_MEDIAROOM].close()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
stop_discovery)
_LOGGER.debug("Auto discovery installed")
hass.data[DISCOVERY_MEDIAROOM] = True
class MediaroomDevice(MediaPlayerDevice):
@ -120,7 +130,7 @@ class MediaroomDevice(MediaPlayerDevice):
self._channel = None
self._optimistic = optimistic
self._state = STATE_PLAYING if optimistic else STATE_STANDBY
self._name = 'Mediaroom {}'.format(device_id)
self._name = 'Mediaroom {}'.format(device_id if device_id else host)
self._available = True
if device_id:
self._unique_id = device_id

View file

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

View file

@ -22,6 +22,7 @@ REQUIREMENTS = ['onkyo-eiscp==1.2.4']
_LOGGER = logging.getLogger(__name__)
CONF_SOURCES = 'sources'
CONF_ZONE2 = 'zone2'
DEFAULT_NAME = 'Onkyo Receiver'
@ -40,6 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES):
{cv.string: cv.string},
vol.Optional(CONF_ZONE2, default=False): cv.boolean,
})
@ -57,6 +59,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
eiscp.eISCP(host), config.get(CONF_SOURCES),
name=config.get(CONF_NAME)))
KNOWN_HOSTS.append(host)
# Add Zone2 if configured
if config.get(CONF_ZONE2):
_LOGGER.debug("Setting up zone 2")
hosts.append(OnkyoDeviceZone2(eiscp.eISCP(host),
config.get(CONF_SOURCES),
name=config.get(CONF_NAME) +
" Zone 2"))
except OSError:
_LOGGER.error("Unable to connect to receiver at %s", host)
else:
@ -98,8 +108,9 @@ class OnkyoDevice(MediaPlayerDevice):
return result
def update(self):
"""Get the latest details from the device."""
"""Get the latest state from the device."""
status = self.command('system-power query')
if not status:
return
if status[1] == 'on':
@ -107,9 +118,11 @@ class OnkyoDevice(MediaPlayerDevice):
else:
self._pwstate = STATE_OFF
return
volume_raw = self.command('volume query')
mute_raw = self.command('audio-muting query')
current_source_raw = self.command('input-selector query')
if not (volume_raw and mute_raw and current_source_raw):
return
@ -147,12 +160,12 @@ class OnkyoDevice(MediaPlayerDevice):
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
"""Return boolean indicating mute status."""
return self._muted
@property
def supported_features(self):
"""Flag media player features that are supported."""
"""Return media player features that are supported."""
return SUPPORT_ONKYO
@property
@ -166,7 +179,7 @@ class OnkyoDevice(MediaPlayerDevice):
return self._source_list
def turn_off(self):
"""Turn off media player."""
"""Turn the media player off."""
self.command('system-power standby')
def set_volume_level(self, volume):
@ -189,3 +202,68 @@ class OnkyoDevice(MediaPlayerDevice):
if source in self._source_list:
source = self._reverse_mapping[source]
self.command('input-selector {}'.format(source))
class OnkyoDeviceZone2(OnkyoDevice):
"""Representation of an Onkyo device's zone 2."""
def update(self):
"""Get the latest state from the device."""
status = self.command('zone2.power=query')
if not status:
return
if status[1] == 'on':
self._pwstate = STATE_ON
else:
self._pwstate = STATE_OFF
return
volume_raw = self.command('zone2.volume=query')
mute_raw = self.command('zone2.muting=query')
current_source_raw = self.command('zone2.selector=query')
if not (volume_raw and mute_raw and current_source_raw):
return
# eiscp can return string or tuple. Make everything tuples.
if isinstance(current_source_raw[1], str):
current_source_tuples = \
(current_source_raw[0], (current_source_raw[1],))
else:
current_source_tuples = current_source_raw
for source in current_source_tuples[1]:
if source in self._source_mapping:
self._current_source = self._source_mapping[source]
break
else:
self._current_source = '_'.join(
[i for i in current_source_tuples[1]])
self._muted = bool(mute_raw[1] == 'on')
self._volume = volume_raw[1] / 80.0
def turn_off(self):
"""Turn the media player off."""
self.command('zone2.power=standby')
def set_volume_level(self, volume):
"""Set volume level, input is range 0..1. Onkyo ranges from 1-80."""
self.command('zone2.volume={}'.format(int(volume*80)))
def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player."""
if mute:
self.command('zone2.muting=on')
else:
self.command('zone2.muting=off')
def turn_on(self):
"""Turn the media player on."""
self.command('zone2.power=on')
def select_source(self, source):
"""Set the input source."""
if source in self._source_list:
source = self._reverse_mapping[source]
self.command('zone2.selector={}'.format(source))

View file

@ -402,3 +402,13 @@ songpal_set_sound_setting:
value:
description: Value to set.
example: 'on'
blackbird_set_all_zones:
description: Set all Blackbird zones to a single source.
fields:
entity_id:
description: Name of any blackbird zone.
example: 'media_player.zone_1'
source:
description: Name of source to switch to.
example: 'Source 1'

View file

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

View file

@ -344,6 +344,42 @@ class LgWebOSDevice(MediaPlayerDevice):
self._current_source = source_dict['label']
self._client.set_input(source_dict['id'])
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
_LOGGER.debug(
"Call play media type <%s>, Id <%s>", media_type, media_id)
if media_type == MEDIA_TYPE_CHANNEL:
_LOGGER.debug("Searching channel...")
partial_match_channel_id = None
for channel in self._client.get_channels():
_LOGGER.debug(
"Checking channel number <%s>, name <%s>, id <%s>...",
channel['channelNumber'],
channel['channelName'],
channel['channelId'])
if media_id == channel['channelNumber']:
_LOGGER.debug(
"Perfect match on channel number: switching!")
self._client.set_channel(channel['channelId'])
return
elif media_id.lower() == channel['channelName'].lower():
_LOGGER.debug(
"Perfect match on channel name: switching!")
self._client.set_channel(channel['channelId'])
return
elif media_id.lower() in channel['channelName'].lower():
_LOGGER.debug(
"Partial match on channel name: saving it...")
partial_match_channel_id = channel['channelId']
if partial_match_channel_id is not None:
_LOGGER.debug(
"Using partial match on channel name: switching!")
self._client.set_channel(partial_match_channel_id)
return
def media_play(self):
"""Send play command."""
self._playing = True

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,6 +8,8 @@ https://home-assistant.io/components/sensor/
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
@ -18,6 +20,13 @@ DOMAIN = 'sensor'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SCAN_INTERVAL = timedelta(seconds=30)
DEVICE_CLASSES = [
'battery', # % of battery that is left
'humidity', # % of humidity in the air
'temperature', # temperature (C/F)
]
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
async def async_setup(hass, config):

View file

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

View file

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

View file

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

View file

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

View file

@ -19,7 +19,8 @@ from homeassistant.const import (
CONF_NAME, CONF_MONITORED_VARIABLES)
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['pyebox==0.1.0']
# pylint: disable=import-error
REQUIREMENTS = [] # ['pyebox==0.1.0'] - disabled because it breaks pip10
_LOGGER = logging.getLogger(__name__)

View file

@ -52,6 +52,13 @@ class EcobeeSensor(Entity):
"""Return the name of the Ecobee sensor."""
return self._name
@property
def device_class(self):
"""Return the device class of the sensor."""
if self.type in ('temperature', 'humidity'):
return self.type
return None
@property
def state(self):
"""Return the state of the sensor."""

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